Added U2F Support for YubiKey

This commit is contained in:
Fabian Stamm 2019-03-12 21:06:09 -04:00
parent aa47e6c92f
commit c54406564c
41 changed files with 2955 additions and 2005 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
node_modules/ node_modules/
views/out/ views/out/
views/.*
lib/ lib/
keys/ keys/
*.old *.old

View File

@ -1,36 +1,37 @@
{ {
"User not found": "Benutzer nicht gefunden", "User not found": "Benutzer nicht gefunden",
"Password or username wrong": "Passwort oder Benutzername falsch", "Password or username wrong": "Passwort oder Benutzername falsch",
"Authorize %s": "Authorize %s", "Authorize %s": "Authorize %s",
"Login": "Einloggen", "Login": "Einloggen",
"You are not logged in or your login is expired": "Du bist nicht länger angemeldet oder deine Anmeldung ist abgelaufen.", "You are not logged in or your login is expired": "Du bist nicht länger angemeldet oder deine Anmeldung ist abgelaufen.",
"Username or Email": "Benutzername oder Email", "Username or Email": "Benutzername oder Email",
"Password": "Passwort", "Password": "Passwort",
"Next": "Weiter", "Next": "Weiter",
"Register": "Registrieren", "Register": "Registrieren",
"Mail": "Mail", "Mail": "Mail",
"Repeat Password": "Passwort wiederholen", "Repeat Password": "Passwort wiederholen",
"Username": "Benutzername", "Username": "Benutzername",
"Name": "Name", "Name": "Name",
"Registration code": "Registrierungs Schlüssel", "Registration code": "Registrierungs Schlüssel",
"You need to select one of the options": "Du musst eine der Optionen auswälen", "You need to select one of the options": "Du musst eine der Optionen auswälen",
"Male": "Mann", "Male": "Mann",
"Female": "Frau", "Female": "Frau",
"Other": "Anderes", "Other": "Anderes",
"Registration code required": "Registrierungs Schlüssel benötigt", "Registration code required": "Registrierungs Schlüssel benötigt",
"Username required": "Benutzername benötigt", "Username required": "Benutzername benötigt",
"Name required": "Name benötigt", "Name required": "Name benötigt",
"Mail required": "Mail benötigt", "Mail required": "Mail benötigt",
"The passwords do not match": "Die Passwörter stimmen nicht überein", "The passwords do not match": "Die Passwörter stimmen nicht überein",
"Password is required": "Password benötigt", "Password is required": "Password benötigt",
"Invalid registration code": "Ungültiger Registrierungs Schlüssel", "Invalid registration code": "Ungültiger Registrierungs Schlüssel",
"Username taken": "Benutzername nicht verfügbar", "Username taken": "Benutzername nicht verfügbar",
"Mail linked with other account": "Mail ist bereits mit einem anderen Account verbunden", "Mail linked with other account": "Mail ist bereits mit einem anderen Account verbunden",
"Registration code already used": "Registrierungs Schlüssel wurde bereits verwendet", "Registration code already used": "Registrierungs Schlüssel wurde bereits verwendet",
"Administration": "Administration", "Administration": "Administration",
"Field {{field}} is not defined": "Feld {{field}} ist nicht deklariert", "Field {{field}} is not defined": "Feld {{field}} ist nicht deklariert",
"Field {{field}} has wrong type. It should be from type {{type}}": "Feld {{field}} hat den falschen Type. Es sollte vom Typ {{type}} sein", "Field {{field}} has wrong type. It should be from type {{type}}": "Feld {{field}} hat den falschen Type. Es sollte vom Typ {{type}} sein",
"Client has no permission for acces password auth": "Dieser Client hat keine Berechtigung password auth zu benutzen", "Client has no permission for acces password auth": "Dieser Client hat keine Berechtigung password auth zu benutzen",
"Invalid token": "Ungültiges Token", "Invalid token": "Ungültiges Token",
"By clicking on ALLOW, you allow this app to access the requested recources.": "Wenn sie ALLOW drücken, berechtigen sie die Applikation die beantragten Resourcen zu benutzen." "By clicking on ALLOW, you allow this app to access the requested recources.": "Wenn sie ALLOW drücken, berechtigen sie die Applikation die beantragten Resourcen zu benutzen.",
"User": "User"
} }

429
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,37 +17,41 @@
"@types/compression": "^0.0.36", "@types/compression": "^0.0.36",
"@types/cookie-parser": "^1.4.1", "@types/cookie-parser": "^1.4.1",
"@types/dotenv": "^6.1.0", "@types/dotenv": "^6.1.0",
"@types/express": "^4.16.0", "@types/express": "^4.16.1",
"@types/handlebars": "^4.0.40", "@types/i18n": "^0.8.5",
"@types/i18n": "^0.8.3",
"@types/ini": "^1.3.30", "@types/ini": "^1.3.30",
"@types/jsonwebtoken": "^8.3.0", "@types/jsonwebtoken": "^8.3.0",
"@types/mongodb": "^3.1.19", "@types/mongodb": "^3.1.19",
"@types/node": "^10.12.18", "@types/node": "^11.9.5",
"@types/node-rsa": "^1.0.0", "@types/node-rsa": "^1.0.0",
"@types/qrcode": "^1.3.1",
"@types/speakeasy": "^2.0.4",
"@types/uuid": "^3.4.4", "@types/uuid": "^3.4.4",
"concurrently": "^4.1.0", "concurrently": "^4.1.0",
"nodemon": "^1.18.9", "nodemon": "^1.18.10",
"typescript": "^3.2.4" "typescript": "^3.3.3333"
}, },
"dependencies": { "dependencies": {
"@hibas123/nodelogging": "^1.3.21", "@hibas123/nodelogging": "^1.3.21",
"@hibas123/nodeloggingserver_client": "^1.1.2", "@hibas123/nodeloggingserver_client": "^1.1.2",
"@hibas123/safe_mongo": "^1.4.5", "@hibas123/safe_mongo": "^1.5.3",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"compression": "^1.7.3", "compression": "^1.7.3",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^6.2.0", "dotenv": "^6.2.0",
"express": "^4.16.4", "express": "^4.16.4",
"handlebars": "^4.0.12", "handlebars": "^4.1.0",
"i18n": "^0.8.3", "i18n": "^0.8.3",
"ini": "^1.3.5", "ini": "^1.3.5",
"jsonwebtoken": "^8.4.0", "jsonwebtoken": "^8.5.0",
"moment": "^2.23.0", "moment": "^2.24.0",
"mongodb": "^3.1.12", "mongodb": "^3.1.13",
"node-rsa": "^1.0.2", "node-rsa": "^1.0.3",
"qrcode": "^1.3.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"speakeasy": "^2.0.0",
"u2f": "^0.1.3",
"uuid": "^3.3.2" "uuid": "^3.3.2"
} }
} }

View File

@ -1,8 +1,8 @@
import { Request, Router } from "express"; import { Request, Router } from "express";
import ClientRoute from "./admin/client"; import ClientRoute from "./client";
import UserRoute from "./admin/user"; import UserRoute from "./user";
import RegCodeRoute from "./admin/regcode"; import RegCodeRoute from "./regcode";
import PermissionRoute from "./admin/permission"; import PermissionRoute from "./permission";
const AdminRoute: Router = Router(); const AdminRoute: Router = Router();
AdminRoute.use("/client", ClientRoute); AdminRoute.use("/client", ClientRoute);

View File

@ -14,6 +14,9 @@ ApiRouter.use("/user", UserRoute);
ApiRouter.use("/internal", InternalRoute); ApiRouter.use("/internal", InternalRoute);
ApiRouter.use("/oauth", OAuthRoute); ApiRouter.use("/oauth", OAuthRoute);
ApiRouter.use("/client/user", AuthGetUser);
// Legacy reasons (deprecated)
ApiRouter.use("/user", AuthGetUser); ApiRouter.use("/user", AuthGetUser);
// Legacy reasons (deprecated) // Legacy reasons (deprecated)

View File

@ -1,6 +1,6 @@
import { Router } from "express"; import { Router } from "express";
import { OAuthInternalApp } from "./internal/oauth"; import { OAuthInternalApp } from "./oauth";
import PasswordAuth from "./internal/password"; import PasswordAuth from "./password";
const InternalRoute: Router = Router(); const InternalRoute: Router = Router();
InternalRoute.get("/oauth", OAuthInternalApp); InternalRoute.get("/oauth", OAuthInternalApp);

View File

@ -1,7 +1,9 @@
import { Request, Response, NextFunction, RequestHandler } from "express"; import { Request, Response, NextFunction, RequestHandler } from "express";
import promiseMiddleware from "../../helper/promiseMiddleware"; import promiseMiddleware from "../../helper/promiseMiddleware";
function call(handler: RequestHandler, req: Request, res: Response) { type RH = (req: Request, res: Response, next?: NextFunction) => any;
function call(handler: RH, req: Request, res: Response) {
return new Promise((yes, no) => { return new Promise((yes, no) => {
let p = handler(req, res, (err) => { let p = handler(req, res, (err) => {
if (err) no(err); if (err) no(err);
@ -11,7 +13,7 @@ function call(handler: RequestHandler, req: Request, res: Response) {
}) })
} }
const Stacker = (...handler: RequestHandler[]) => { const Stacker = (...handler: RH[]) => {
return promiseMiddleware(async (req: Request, res: Response, next: NextFunction) => { return promiseMiddleware(async (req: Request, res: Response, next: NextFunction) => {
let hc = handler.concat(); let hc = handler.concat();
while (hc.length > 0) { while (hc.length > 0) {

View File

@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import LoginToken from "../../models/login_token"; import LoginToken, { CheckToken } from "../../models/login_token";
import Logging from "@hibas123/nodelogging"; import Logging from "@hibas123/nodelogging";
import RequestError, { HttpStatusCode } from "../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../helper/request_error";
import User from "../../models/user"; import User from "../../models/user";
@ -14,7 +14,7 @@ class Invalid extends Error { }
* @param json Checks if requests wants an json or html for returning errors * @param json Checks if requests wants an json or html for returning errors
* @param redirect_uri Sets the uri to redirect, if json is not set and user not logged in * @param redirect_uri Sets the uri to redirect, if json is not set and user not logged in
*/ */
export function GetUserMiddleware(json = false, special_token: boolean = false, redirect_uri?: string) { export function GetUserMiddleware(json = false, special_required: boolean = false, redirect_uri?: string, validated = true) {
return promiseMiddleware(async function (req: Request, res: Response, next?: NextFunction) { return promiseMiddleware(async function (req: Request, res: Response, next?: NextFunction) {
const invalid = () => { const invalid = () => {
throw new Invalid(); throw new Invalid();
@ -24,8 +24,7 @@ export function GetUserMiddleware(json = false, special_token: boolean = false,
if (!login) invalid() if (!login) invalid()
let token = await LoginToken.findOne({ token: login, valid: true }) let token = await LoginToken.findOne({ token: login, valid: true })
if (!token) invalid() if (!await CheckToken(token, validated)) invalid();
if (!token.validated) invalid();
let user = await User.findById(token.user); let user = await User.findById(token.user);
if (!user) { if (!user) {
@ -34,31 +33,23 @@ export function GetUserMiddleware(json = false, special_token: boolean = false,
invalid(); invalid();
} }
if (token.validTill.getTime() < new Date().getTime()) { //Token expired let special_token;
token.valid = false;
await LoginToken.save(token);
invalid()
}
if (special) { if (special) {
Logging.debug("Special found") Logging.debug("Special found")
let st = await LoginToken.findOne({ token: special, special: true, valid: true }) special_token = await LoginToken.findOne({ token: special, special: true, valid: true, user: token.user })
if (st && st.validated && st.valid && st.user.toHexString() === token.user.toHexString()) { if (!await CheckToken(special_token, validated))
if (st.validTill.getTime() < new Date().getTime()) { //Token expired invalid();
Logging.debug("Special expired") req.special = true;
st.valid = false;
await LoginToken.save(st);
} else {
Logging.debug("Special valid")
req.special = true;
}
}
} }
if (special_token && !req.special) invalid(); if (special_required && !req.special) invalid();
req.user = user req.user = user
req.isAdmin = user.admin; req.isAdmin = user.admin;
req.token = {
login: token,
special: special_token
}
if (next) if (next)
next() next()
@ -67,7 +58,7 @@ export function GetUserMiddleware(json = false, special_token: boolean = false,
if (e instanceof Invalid) { if (e instanceof Invalid) {
if (req.method === "GET" && !json) { if (req.method === "GET" && !json) {
res.status(HttpStatusCode.UNAUTHORIZED) res.status(HttpStatusCode.UNAUTHORIZED)
res.redirect("/login?base64=true&state=" + new Buffer(redirect_uri ? redirect_uri : req.originalUrl).toString("base64")) res.redirect("/login?base64=true&state=" + Buffer.from(redirect_uri ? redirect_uri : req.originalUrl).toString("base64"))
} else { } else {
throw new RequestError(req.__("You are not logged in or your login is expired"), HttpStatusCode.UNAUTHORIZED) throw new RequestError(req.__("You are not logged in or your login is expired"), HttpStatusCode.UNAUTHORIZED)
} }

View File

@ -1,8 +1,8 @@
import { Router } from "express"; import { Router } from "express";
import AuthRoute from "./oauth/auth"; import AuthRoute from "./auth";
import JWTRoute from "./oauth/jwt"; import JWTRoute from "./jwt";
import Public from "./oauth/public"; import Public from "./public";
import RefreshTokenRoute from "./oauth/refresh"; import RefreshTokenRoute from "./refresh";
const OAuthRoue: Router = Router(); const OAuthRoue: Router = Router();
OAuthRoue.post("/auth", AuthRoute); OAuthRoue.post("/auth", AuthRoute);

View File

@ -1,8 +0,0 @@
import { Request, Router } from "express";
import Register from "./user/register";
import Login from "./user/login";
const UserRoute: Router = Router();
UserRoute.post("/register", Register);
UserRoute.post("/login", Login)
export default UserRoute;

14
src/api/user/index.ts Normal file
View File

@ -0,0 +1,14 @@
import { Router } from "express";
import Register from "./register";
import Login from "./login";
import TwoFactorRoute from "./twofactor";
import { GetToken, DeleteToken } from "./token";
const UserRoute: Router = Router();
UserRoute.post("/register", Register);
UserRoute.post("/login", Login)
UserRoute.use("/twofactor", TwoFactorRoute);
UserRoute.get("/token", GetToken);
UserRoute.delete("/token", DeleteToken);
export default UserRoute;

View File

@ -1,10 +1,12 @@
import { Request, Response } from "express" import { Request, Response } from "express"
import User, { IUser, TokenTypes } from "../../models/user"; import User, { IUser } from "../../models/user";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import moment = require("moment"); import moment = require("moment");
import LoginToken from "../../models/login_token"; import LoginToken from "../../models/login_token";
import RequestError, { HttpStatusCode } from "../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../helper/request_error";
import promiseMiddleware from "../../helper/promiseMiddleware"; import promiseMiddleware from "../../helper/promiseMiddleware";
import * as speakeasy from "speakeasy";
import TwoFactor from "../../models/twofactor";
const Login = promiseMiddleware(async (req: Request, res: Response) => { const Login = promiseMiddleware(async (req: Request, res: Response) => {
let type = req.query.type; let type = req.query.type;
@ -19,7 +21,13 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
return; return;
} }
const sendToken = async (user: IUser, tfa?: TokenTypes[]) => { const sendToken = async (user: IUser, tfa?: any[]) => {
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress
let client = {
ip: Array.isArray(ip) ? ip[0] : ip,
browser: req.headers["user-agent"]
}
let token_str = randomBytes(16).toString("hex"); let token_str = randomBytes(16).toString("hex");
let tfa_exp = moment().add(5, "minutes").toDate() let tfa_exp = moment().add(5, "minutes").toDate()
let token_exp = moment().add(6, "months").toDate() let token_exp = moment().add(6, "months").toDate()
@ -28,7 +36,8 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
valid: true, valid: true,
validTill: tfa ? tfa_exp : token_exp, validTill: tfa ? tfa_exp : token_exp,
user: user._id, user: user._id,
validated: tfa ? false : true validated: tfa ? false : true,
...client
}); });
await LoginToken.save(token); await LoginToken.save(token);
@ -40,7 +49,8 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
validTill: tfa ? tfa_exp : special_exp, validTill: tfa ? tfa_exp : special_exp,
special: true, special: true,
user: user._id, user: user._id,
validated: tfa ? false : true validated: tfa ? false : true,
...client
}); });
await LoginToken.save(special); await LoginToken.save(special);
@ -51,7 +61,7 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
}); });
} }
if (type === "password" || type === "twofactor") { if (type === "password") {
let { username, password, uid } = req.body; let { username, password, uid } = req.body;
let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid }) let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid })
@ -61,20 +71,29 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
if (user.password !== password) { if (user.password !== password) {
res.json({ error: req.__("Password or username wrong") }) res.json({ error: req.__("Password or username wrong") })
} else { } else {
if (type === "twofactor") { let twofactor = await TwoFactor.find({ user: user._id, valid: true })
let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false)
await Promise.all(expired.map(e => {
e.valid = false;
return TwoFactor.save(e);
}));
twofactor = twofactor.filter(e => e.valid);
if (twofactor && twofactor.length > 0) {
let tfa = twofactor.map(e => {
return {
id: e._id,
name: e.name,
type: e.type
}
})
await sendToken(user, tfa);
} else { } else {
if (user.twofactor && user.twofactor.length > 0) { await sendToken(user);
let types = user.twofactor.filter(f => f.valid).map(f => f.type)
await sendToken(user, types);
} else {
await sendToken(user);
}
} }
} }
} }
} else { } else {
throw new RequestError("Invalid type!", HttpStatusCode.BAD_REQUEST); res.json({ error: req.__("Invalid type!") });
} }
}); });

29
src/api/user/token.ts Normal file
View File

@ -0,0 +1,29 @@
import { Request, Response } from "express";
import Stacker from "../middlewares/stacker";
import { GetUserMiddleware } from "../middlewares/user";
import LoginToken, { CheckToken } from "../../models/login_token";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
export const GetToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => {
let raw_token = await LoginToken.find({ user: req.user._id, valid: true });
let token = await Promise.all(raw_token.map(async token => {
await CheckToken(token);
return {
id: token._id,
special: token.special,
ip: token.ip,
browser: token.browser,
isthis: token._id.equals(token.special ? req.token.special._id : req.token.login._id)
}
}).filter(t => t !== undefined));
res.json({ token });
});
export const DeleteToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => {
let { id } = req.query;
let token = await LoginToken.findById(id);
if (!token || !token.user.equals(req.user._id)) throw new RequestError("Invalid ID", HttpStatusCode.BAD_REQUEST);
token.valid = false;
await LoginToken.save(token);
res.json({ success: true });
});

View File

@ -0,0 +1,79 @@
import LoginToken, { ILoginToken } from "../../../models/login_token";
import moment = require("moment");
// export async function unlockToken() {
// let { type, code, login, special } = req.body;
// let [login_t, special_t] = await Promise.all([LoginToken.findOne({ token: login }), LoginToken.findOne({ token: special })]);
// if ((login && !login_t) || (special && !special_t)) {
// res.json({ error: req.__("Token not found!") });
// } else {
// let atoken = special_t || login_t;
// let user = await User.findById(atoken.user);
// let tf = await TwoFactor.find({ user: user._id, valid: true })
// let valid = false;
// switch (type) {
// case TokenTypes.OTC: {
// let twofactor = await TwoFactor.findOne({ type, valid: true })
// if (twofactor) {
// valid = speakeasy.totp.verify({
// secret: twofactor.token,
// encoding: "base64",
// token: code
// })
// }
// break;
// }
// case TokenTypes.BACKUP_CODE: {
// let twofactor = await TwoFactor.findOne({ type, valid: true, token: code })
// if (twofactor) {
// twofactor.valid = false;
// await TwoFactor.save(twofactor);
// valid = true;
// }
// break;
// }
// case TokenTypes.APP_ALLOW:
// case TokenTypes.YUBI_KEY:
// default:
// res.json({ error: req.__("Invalid twofactor!") });
// return;
// }
// if (!valid) {
// res.json({ error: req.__("Invalid code!") });
// return;
// }
// let result: any = {};
// if (login_t) {
// login_t.validated = true
// await LoginToken.save(login_t)
// result.login = { token: login_t.token, expires: login_t.validTill.toUTCString() }
// }
// if (special_t) {
// special_t.validated = true;
// await LoginToken.save(special_t);
// result.special = { token: special_t.token, expires: special_t.validTill.toUTCString() }
// }
// res.json(result);
// }
// }
export async function upgradeToken(token: ILoginToken) {
token.data = undefined;
token.valid = true;
token.validated = true;
//TODO durations from config
let expires = (token.special ? moment().add(30, "minute") : moment().add(6, "months")).toDate();
token.validTill = expires;
await LoginToken.save(token);
return expires;
}

View File

@ -0,0 +1,43 @@
import { Router } from "express";
import YubiKeyRoute from "./yubikey";
import { GetUserMiddleware } from "../../middlewares/user";
import Stacker from "../../middlewares/stacker";
import TwoFactor from "../../../models/twofactor";
import * as moment from "moment"
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
const TwoFactorRouter = Router();
TwoFactorRouter.get("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
let twofactor = await TwoFactor.find({ user: req.user._id, valid: true })
let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false)
await Promise.all(expired.map(e => {
e.valid = false;
return TwoFactor.save(e);
}));
twofactor = twofactor.filter(e => e.valid);
let tfa = twofactor.map(e => {
return {
id: e._id,
name: e.name,
type: e.type
}
})
res.json({ methods: tfa });
}));
TwoFactorRouter.delete("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
let { id } = req.query;
let tfa = await TwoFactor.findById(id);
if (!tfa || !tfa.user.equals(req.user._id)) {
throw new RequestError("Invalid id", HttpStatusCode.BAD_REQUEST);
}
tfa.valid = false;
await TwoFactor.save(tfa);
res.json({ success: true });
}));
TwoFactorRouter.use("/yubikey", YubiKeyRoute);
export default TwoFactorRouter;

View File

@ -0,0 +1,131 @@
import { Router, Request } from "express"
import Stacker from "../../../middlewares/stacker";
import { UserMiddleware, GetUserMiddleware } from "../../../middlewares/user";
import * as u2f from "u2f";
import config from "../../../../config";
import TwoFactor, { TFATypes as TwoFATypes, IYubiKey } from "../../../../models/twofactor";
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
import moment = require("moment");
import LoginToken from "../../../../models/login_token";
import { upgradeToken } from "../helper";
import Logging from "@hibas123/nodelogging";
const U2FRoute = Router();
U2FRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
const { type } = req.query;
if (type === "challenge") {
const registrationRequest = u2f.request(config.core.url);
let twofactor = TwoFactor.new(<IYubiKey>{
user: req.user._id,
type: TwoFATypes.U2F,
valid: false,
data: {
registration: registrationRequest
}
})
await TwoFactor.save(twofactor);
res.json({ request: registrationRequest, id: twofactor._id, appid: config.core.url });
} else {
const { response, id } = req.body;
Logging.debug(req.body, id);
let twofactor: IYubiKey = await TwoFactor.findById(id);
const err = () => { throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST) };
if (!twofactor || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.U2F || !twofactor.data.registration || twofactor.valid) {
Logging.debug("Not found or wrong user", twofactor);
err();
}
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
await TwoFactor.delete(twofactor);
Logging.debug("Expired!", twofactor);
err();
}
const result = u2f.checkRegistration(twofactor.data.registration, response);
if (result.successful) {
twofactor.data = {
keyHandle: result.keyHandle,
publicKey: result.publicKey
}
twofactor.expires = undefined;
twofactor.valid = true;
await TwoFactor.save(twofactor);
res.json({ success: true });
} else {
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST);
}
}
}));
U2FRoute.get("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => {
let { login, special } = req.token;
let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true })
if (!twofactor) {
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
}
if (twofactor.expires) {
if (moment().isAfter(twofactor.expires)) {
twofactor.valid = false;
await TwoFactor.save(twofactor);
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
}
}
let request = u2f.request(config.core.url, twofactor.data.keyHandle);
login.data = {
type: "ykr",
request
};
let r;;
if (special) {
special.data = login.data;
r = LoginToken.save(special);
}
await Promise.all([r, LoginToken.save(login)]);
res.json({ request });
}))
U2FRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => {
let { login, special } = req.token;
let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true })
let { response } = req.body;
if (!twofactor || !login.data || login.data.type !== "ykr" || special && (!special.data || special.data.type !== "ykr")) {
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
}
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
twofactor.valid = false;
await TwoFactor.save(twofactor);
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
}
let login_exp;
let special_exp;
let result = u2f.checkSignature(login.data.request, response, twofactor.data.publicKey);
if (result.successful) {
if (special) {
let result = u2f.checkSignature(special.data.request, response, twofactor.data.publicKey);
if (result.successful) {
special_exp = await upgradeToken(special);
}
else {
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST);
}
}
login_exp = await upgradeToken(login);
}
else {
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST);
}
res.json({ success: true, login_exp, special_exp })
}))
export default U2FRoute;

View File

@ -10,6 +10,8 @@ export interface WebConfig {
export interface CoreConfig { export interface CoreConfig {
name: string name: string
url: string
dev: string
} }
export interface Config { export interface Config {
@ -31,11 +33,12 @@ import * as dotenv from "dotenv";
import { Logging } from "@hibas123/nodelogging"; import { Logging } from "@hibas123/nodelogging";
dotenv.config(); dotenv.config();
const config: Config = ini.parse(readFileSync("./config.ini").toString()) const config = ini.parse(readFileSync("./config.ini").toString()) as Config;
if (config.dev) config.dev = Boolean(config.dev);
if (process.env.DEV === "true") { if (config.core.dev) config.dev = Boolean(config.core.dev);
if (process.env.DEV === "true")
config.dev = true; config.dev = true;
if (config.dev)
Logging.warning("DEV mode active. This can cause major performance issues, data loss and vulnerabilities! ") Logging.warning("DEV mode active. This can cause major performance issues, data loss and vulnerabilities! ")
}
export default config; export default config;

5
src/express.d.ts vendored
View File

@ -1,5 +1,6 @@
import { IUser } from "./models/user"; import { IUser } from "./models/user";
import { IClient } from "./models/client"; import { IClient } from "./models/client";
import { ILoginToken } from "./models/login_token";
declare module "express" { declare module "express" {
interface Request { interface Request {
@ -7,5 +8,9 @@ declare module "express" {
client: IClient; client: IClient;
isAdmin: boolean; isAdmin: boolean;
special: boolean; special: boolean;
token: {
login: ILoginToken;
special?: ILoginToken;
}
} }
} }

View File

@ -1,6 +1,7 @@
import DB from "../database"; import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model"; import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb"; import { ObjectID } from "mongodb";
import moment = require("moment");
export interface ILoginToken extends ModelDataBase { export interface ILoginToken extends ModelDataBase {
token: string; token: string;
@ -9,6 +10,9 @@ export interface ILoginToken extends ModelDataBase {
validTill: Date; validTill: Date;
valid: boolean; valid: boolean;
validated: boolean; validated: boolean;
data: any;
ip: string;
browser: string;
} }
const LoginToken = DB.addModel<ILoginToken>({ const LoginToken = DB.addModel<ILoginToken>({
name: "login_token", name: "login_token",
@ -31,7 +35,31 @@ const LoginToken = DB.addModel<ILoginToken>({
valid: { type: Boolean }, valid: { type: Boolean },
validated: { type: Boolean, default: false } validated: { type: Boolean, default: false }
} }
}, {
migration: (doc: ILoginToken) => { doc.validated = true; },
schema: {
token: { type: String },
special: { type: Boolean, default: () => false },
user: { type: ObjectID },
validTill: { type: Date },
valid: { type: Boolean },
validated: { type: Boolean, default: false },
data: { type: "any", optional: true },
ip: { type: String, optional: true },
browser: { type: String, optional: true }
}
}] }]
}) })
export async function CheckToken(token: ILoginToken, validated: boolean = true): Promise<boolean> {
if (!token || !token.valid) return false;
if (validated && !token.validated) return false;
if (moment().isAfter(token.validTill)) {
token.valid = false;
await LoginToken.save(token)
return false;
}
return true;
}
export default LoginToken; export default LoginToken;

57
src/models/twofactor.ts Normal file
View File

@ -0,0 +1,57 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "bson";
export enum TFATypes {
OTC,
BACKUP_CODE,
U2F,
APP_ALLOW
}
export interface ITwoFactor extends ModelDataBase {
user: ObjectID
valid: boolean
expires?: Date;
name?: string;
type: TFATypes
data: any;
}
export interface IOTP extends ITwoFactor {
data: string;
}
export interface IYubiKey extends ITwoFactor {
data: {
registration?: any;
publicKey: string;
keyHandle: string;
}
}
export interface IU2F extends ITwoFactor {
data: {
challenge?: string;
publicKey: string;
keyHandle: string;
registration?: string;
}
}
const TwoFactor = DB.addModel<ITwoFactor>({
name: "twofactor",
versions: [{
migration: (e) => { },
schema: {
user: { type: ObjectID },
valid: { type: Boolean },
expires: { type: Date, optional: true },
name: { type: String, optional: true },
type: { type: Number },
data: { type: "any" },
}
}]
});
export default TwoFactor;

View File

@ -11,11 +11,6 @@ export enum Gender {
other other
} }
export enum TokenTypes {
OTC,
BACKUP_CODE
}
export interface IUser extends ModelDataBase { export interface IUser extends ModelDataBase {
uid: string; uid: string;
username: string; username: string;
@ -28,7 +23,6 @@ export interface IUser extends ModelDataBase {
salt: string; salt: string;
mails: ObjectID[]; mails: ObjectID[];
phones: { phone: string, verified: boolean, primary: boolean }[]; phones: { phone: string, verified: boolean, primary: boolean }[];
twofactor: { token: string, valid: boolean, type: TokenTypes }[];
encryption_key: string; encryption_key: string;
} }
@ -100,6 +94,33 @@ const User = DB.addModel<IUser>({
default: () => randomString(64) default: () => randomString(64)
} }
} }
},
{
migration: (e: any) => { delete e.twofactor },
schema: {
uid: { type: String, default: () => v4() },
username: { type: String },
name: { type: String },
birthday: { type: Date, optional: true },
gender: { type: Number },
admin: { type: Boolean },
password: { type: String },
salt: { type: String },
mails: { type: Array, default: () => [] },
phones: {
array: true,
model: true,
type: {
phone: { type: String },
verified: { type: Boolean },
primary: { type: Boolean }
}
},
encryption_key: {
type: String,
default: () => randomString(64)
}
}
}] }]
}) })

View File

@ -6,6 +6,7 @@ import * as moment from "moment";
import Permission from "./models/permissions"; import Permission from "./models/permissions";
import { ObjectID } from "bson"; import { ObjectID } from "bson";
import DB from "./database"; import DB from "./database";
import TwoFactor from "./models/twofactor";
export default async function TestData() { export default async function TestData() {
await DB.db.dropDatabase(); await DB.db.dropDatabase();
@ -61,4 +62,19 @@ export default async function TestData() {
}) })
await RegCode.save(r); await RegCode.save(r);
} }
let t = await TwoFactor.findOne({ user: u._id, type: 2 })
if (!t) {
t = TwoFactor.new({
user: u._id,
type: 2,
valid: true,
data: {
keyHandle: "tWSaMoHX2E96CoZOKOi_4aj6WVEh1e46FKXN0oDY2Z-laNOFcATlStNDo52HX7ygupW-v9qZOCX3J4d5nhOzWQ",
publicKey: "BPsgBxR8M7MyrknlFuvYZv0Z1lZxiJQJNrLDA1yi3XKD_lrhIpnAh2OY_TsFjASvn3JTtwlCh62QdMvN-ejQL78"
},
expires: null
})
TwoFactor.save(t);
}
} }

View File

@ -8,6 +8,7 @@ function loadStatic() {
let html = readFileSync("./views/out/login/login.html").toString(); let html = readFileSync("./views/out/login/login.html").toString();
template = handlebars.compile(html); template = handlebars.compile(html);
} }
/** /**
* Benchmarks (5000, 500 cuncurrent) * Benchmarks (5000, 500 cuncurrent)
* Plain: * Plain:
@ -26,7 +27,6 @@ function loadStatic() {
* - prod 6sec * - prod 6sec
*/ */
export default function GetLoginPage(__: typeof i__): string { export default function GetLoginPage(__: typeof i__): string {
if (config.dev) { if (config.dev) {
loadStatic() loadStatic()

View File

@ -13,10 +13,11 @@ import Client from "../models/client";
import { Logging } from "@hibas123/nodelogging"; import { Logging } from "@hibas123/nodelogging";
import Stacker from "../api/middlewares/stacker"; import Stacker from "../api/middlewares/stacker";
import { UserMiddleware, GetUserMiddleware } from "../api/middlewares/user"; import { UserMiddleware, GetUserMiddleware } from "../api/middlewares/user";
import GetUserPage from "./user";
Handlebars.registerHelper("appname", () => config.core.name); Handlebars.registerHelper("appname", () => config.core.name);
const cacheTime = moment.duration(1, "month").asSeconds(); const cacheTime = config.dev ? moment.duration(1, "month").asSeconds() : 10;
const ViewRouter: IRouter<void> = Router(); const ViewRouter: IRouter<void> = Router();
ViewRouter.get("/", UserMiddleware, (req, res) => { ViewRouter.get("/", UserMiddleware, (req, res) => {
@ -40,6 +41,11 @@ ViewRouter.get("/admin", GetUserMiddleware(false, true), (req: Request, res, nex
res.send(GetAdminPage(req.__)) res.send(GetAdminPage(req.__))
}) })
ViewRouter.get("/user", Stacker(GetUserMiddleware(false, true), (req, res) => {
res.setHeader("Cache-Control", "public, max-age=" + cacheTime);
res.send(GetUserPage(req.__));
}));
ViewRouter.get("/auth", Stacker(GetUserMiddleware(false, true), async (req, res) => { ViewRouter.get("/auth", Stacker(GetUserMiddleware(false, true), async (req, res) => {
let { scope, redirect_uri, state, client_id }: { [key: string]: string } = req.query; let { scope, redirect_uri, state, client_id }: { [key: string]: string } = req.query;
const sendError = (type) => { const sendError = (type) => {
@ -85,7 +91,7 @@ if (config.dev) {
res.send(GetAuthPage(req.__, "Test 05265", [ res.send(GetAuthPage(req.__, "Test 05265", [
{ {
name: "Access Profile", name: "Access Profile",
description: "It allows the application to know who you are. Required for all applications.", description: "It allows the application to know who you are. Required for all applications. And a lot of more Text, because why not? This will not stop, till it is multiple lines long and maybe kill the layout, so keep reading as long as you like, but I promise it will get boring after some time. So this should be enougth.",
logo: logo logo: logo
}, },
{ {

View File

@ -9,7 +9,7 @@ import * as cookieparser from "cookie-parser"
import * as i18n from "i18n" import * as i18n from "i18n"
import * as compression from "compression"; import * as compression from "compression";
import ApiRouter from "./api/api"; import ApiRouter from "./api";
import ViewRouter from "./views/views"; import ViewRouter from "./views/views";
import RequestError, { HttpStatusCode } from "./helper/request_error"; import RequestError, { HttpStatusCode } from "./helper/request_error";
@ -64,16 +64,14 @@ export default class Web {
next() next()
}) })
function shouldCompress(req, res) { this.server.use(compression({
if (req.headers['x-no-compression']) { filter: (req, res) => {
// don't compress responses with this request header if (req.headers['x-no-compression']) {
return false return false
}
return compression.filter(req, res)
} }
}));
// fallback to standard filter function
return compression.filter(req, res)
}
this.server.use(compression({ filter: shouldCompress }))
} }
private registerEndpoints() { private registerEndpoints() {

View File

@ -4,13 +4,16 @@ const {
mkdirSync, mkdirSync,
copyFileSync, copyFileSync,
writeFileSync, writeFileSync,
readFileSync readFileSync,
exists
} = require('fs') } = require('fs')
const { const {
join, join,
basename basename,
dirname
} = require('path') } = require('path')
const includepaths = require("rollup-plugin-includepaths")
const isDirectory = source => lstatSync(source).isDirectory() const isDirectory = source => lstatSync(source).isDirectory()
const getDirectories = source => const getDirectories = source =>
@ -24,6 +27,8 @@ function ensureDir(folder) {
} }
} }
const fileExists = (filename) => new Promise((yes, no) => exists(filename, (exi) => yes(exi)));
ensureDir("./out") ensureDir("./out")
const sass = require('sass'); const sass = require('sass');
@ -38,29 +43,77 @@ function findHead(elm) {
} }
const rollup = require("rollup") const rollup = require("rollup")
const includepaths = require("rollup-plugin-includepaths")
const typescript = require("rollup-plugin-typescript2");
const resolve = require("rollup-plugin-node-resolve");
const minify = require("html-minifier").minify const minify = require("html-minifier").minify
const gzipSize = require('gzip-size'); const gzipSize = require('gzip-size');
async function buildPage(folder, name) { async function file_name(folder, name, exts) {
for (let ext of exts) {
let basefile = `${folder}/${name}.${ext}`;
if (await fileExists(basefile)) return basefile;
}
return null;
}
async function buildPage(folder) {
const pagename = basename(folder); const pagename = basename(folder);
const outpath = "./out/" + pagename; const outpath = "./out/" + pagename;
ensureDir(outpath) ensureDir(outpath)
const basefile = await file_name(folder, pagename, ["tsx", "ts", "js"]);
let bundle = await rollup.rollup({ let bundle = await rollup.rollup({
input: `${folder}/${pagename}.js`, input: basefile,
plugins: [includepaths({ plugins: [
paths: ["shared"] includepaths({
})], paths: ["shared", "node_modules"]
}),
typescript(),
resolve({
// use "module" field for ES6 module if possible
module: true, // Default: true
// use "jsnext:main" if possible
// legacy field pointing to ES6 module in third-party libraries,
// deprecated in favor of "pkg.module":
// - see: https://github.com/rollup/rollup/wiki/pkg.module
jsnext: true, // Default: false
// use "main" field or index.js, even if it's not an ES6 module
// (needs to be converted from CommonJS to ES6
// see https://github.com/rollup/rollup-plugin-commonjs
main: true, // Default: true
// some package.json files have a `browser` field which
// specifies alternative files to load for people bundling
// for the browser. If that's you, use this option, otherwise
// pkg.browser will be ignored
browser: true, // Default: false
// not all files you want to resolve are .js files
extensions: ['.mjs', '.js', '.jsx', '.json'], // Default: [ '.mjs', '.js', '.json', '.node' ]
// whether to prefer built-in modules (e.g. `fs`, `path`) or
// local ones with the same names
preferBuiltins: false, // Default: true
// If true, inspect resolved files to check that they are
// ES2015 modules
modulesOnly: true, // Default: false
})
],
treeshake: true
}) })
let { let { output } = await bundle.generate({
code,
map
} = await bundle.generate({
format: "iife", format: "iife",
compact: true
}) })
let { code } = output[0];
let sass_res = sass.renderSync({ let sass_res = sass.renderSync({
file: folder + `/${pagename}.scss`, file: folder + `/${pagename}.scss`,
@ -94,15 +147,14 @@ async function buildPage(folder, name) {
collapseWhitespace: true, collapseWhitespace: true,
html5: true, html5: true,
keepClosingSlash: true, keepClosingSlash: true,
minifyCSS: true, minifyCSS: false,
minifyJS: true, minifyJS: false,
removeComments: true, removeComments: true,
useShortDoctype: true useShortDoctype: true
}) })
let gzips = await gzipSize(result) let gzips = await gzipSize(result)
writeFileSync(`${outpath}/${pagename}.html`, result) writeFileSync(`${outpath}/${pagename}.html`, result)
let stats = { let stats = {
sass: sass_res.stats, sass: sass_res.stats,
js: { js: {
@ -119,42 +171,24 @@ async function buildPage(folder, name) {
} }
async function run() { async function run() {
const pages = getDirectories("./src"); console.log("Start compiling!");
const ProgressBar = require('progress'); let pages = getDirectories("./src");
// const bar = new ProgressBar('[:bar] :current/:total :percent :elapseds :etas', {
// // schema: '[:bar] :current/:total :percent :elapseds :etas',
// total: pages.length
// });
await Promise.all(pages.map(async e => { await Promise.all(pages.map(async e => {
try { try {
await buildPage(e) await buildPage(e)
} catch (er) { } catch (er) {
console.error("Failed compiling", basename(e)) console.error("Failed compiling", basename(e))
console.log(er.message) console.log(er)
} }
// bar.tick()
})) }))
console.log("Finished compiling!") console.log("Finished compiling!")
} }
if (process.argv.join(" ").toLowerCase().indexOf("watch") < 0) { const chokidar = require("chokidar");
run() if (process.argv.join(" ").toLowerCase().indexOf("watch") >= 0)
} else { chokidar.watch(["./src", "./node_modules", "./package.json", "./package-lock.json"], {
const nodemon = require('nodemon'); ignoreInitial: true
})
nodemon({ .on("all", () => run());
script: "dummy.js", run()
ext: 'js hbs scss',
ignore: ["out/"]
});
nodemon.on('start', function () {
run()
}).on('quit', function () {
process.exit();
}).on('restart', function (files) {
// console.log('App restarted due to: ', files);
});
}

2395
views/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,19 +9,20 @@
"watch": "node build.js watch" "watch": "node build.js watch"
}, },
"dependencies": { "dependencies": {
"@material/button": "^0.41.0", "@material/button": "^0.44.1",
"@material/form-field": "^0.41.0", "@material/form-field": "^0.44.1",
"@material/radio": "^0.41.0", "@material/radio": "^0.44.1",
"ascii-progress": "^1.0.5", "preact": "^8.4.2"
"html-minifier": "^3.5.21",
"jsdom": "^13.0.0",
"nodemon": "^1.18.6",
"progress": "^2.0.1",
"rollup": "^0.67.0",
"rollup-plugin-includepaths": "^0.2.3",
"sass": "^1.14.3"
}, },
"devDependencies": { "devDependencies": {
"gzip-size": "^5.0.0" "chokidar": "^2.1.2",
"gzip-size": "^5.0.0",
"html-minifier": "^3.5.21",
"rollup": "^1.3.0",
"rollup-plugin-includepaths": "^0.2.3",
"rollup-plugin-node-resolve": "^4.0.1",
"rollup-plugin-typescript2": "^0.19.3",
"sass": "^1.17.2",
"typescript": "^3.3.3333"
} }
} }

View File

@ -1,13 +1,33 @@
document.querySelectorAll(".floating>input").forEach(e => { (() => {
function checkState() { const run = () => {
if (e.value !== "") { document.querySelectorAll(".floating>input").forEach(e => {
if (e.classList.contains("used")) return; function checkState() {
e.classList.add("used") if (e.value !== "") {
} else { if (e.classList.contains("used")) return;
if (e.classList.contains("used")) e.classList.remove("used") e.classList.add("used")
} } else {
if (e.classList.contains("used")) e.classList.remove("used")
}
}
e.addEventListener("change", () => checkState())
checkState()
})
} }
e.addEventListener("change", () => checkState()) run();
checkState()
}) var mutationObserver = new MutationObserver(() => {
run()
});
mutationObserver.observe(document.documentElement, {
attributes: false,
characterData: false,
childList: true,
subtree: true,
});
window.Mutt
window.addEventListener("DOMNodeInserted", () => run())
})();

2
views/shared/preact.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -17,11 +17,11 @@
<ul> <ul>
{{#scopes}} {{#scopes}}
<li> <li>
<div style="display:inline-block"> <div class="permission">
{{#if logo}} {{#if logo}}
<div class="image"> {{!-- <div class="image"> --}}
<img width="50px" height="50px" src="{{logo}}"> <img class="image" src="{{logo}}">
</div> {{!-- </div> --}}
{{/if}} {{/if}}
<div class="text"> <div class="text">
<h3 class="scope_title">{{name}}</h3> <h3 class="scope_title">{{name}}</h3>

View File

@ -34,20 +34,27 @@ ul {
padding-left: 0; padding-left: 0;
} }
.image { .permission {
display: block; display: flex;
height: 50px; img {
width: 50px; height: 50px;
float: left; width: 50px;
}
.text {
// width: calc(100% - 60px);
padding-left: 10px;
}
} }
.text { // .image {
display: block; // height: 50px;
width: calc(100% - 60px); // width: 50px;
height: 50px; // }
float: right;
padding-left: 10px; // .text {
} // // width: calc(100% - 60px);
// padding-left: 10px;
// }
.scope_title { .scope_title {
margin-top: 0; margin-top: 0;

View File

@ -7,7 +7,8 @@
</head> </head>
<body> <body>
<hgroup> <div id="content"></div>
{{!-- <hgroup>
<h1>{{i18n "Login"}}</h1> <h1>{{i18n "Login"}}</h1>
</hgroup> </hgroup>
<form action="JavaScript:void(0)"> <form action="JavaScript:void(0)">
@ -15,33 +16,41 @@
<div class="loader"></div> <div class="loader"></div>
</div> </div>
<div id="container"> <div id="container">
<div class="floating group" id="usernamegroup"> <div id="usernamegroup">
<input type="text" id="username" autofocus> <div class="floating group">
<span class="highlight"></span> <input type="text" id="username" autofocus>
<span class="bar"></span> <span class="highlight"></span>
<label>{{i18n "Username or Email"}}</label> <span class="bar"></span>
<div class="error invisible" id="uerrorfield"></div> <label>{{i18n "Username or Email"}}</label>
<div class="error invisible" id="uerrorfield"></div>
</div>
<button type="button" id="nextbutton" class="mdc-button mdc-button--raised">{{i18n "Next"}}
</button>
</div> </div>
<div class="floating group invisible" id="passwordgroup"> <div id="passwordgroup">
<input type="password" id="password"> <div class="floating group invisible" id="passwordgroup">
<span class="highlight"></span> <input type="password" id="password">
<span class="bar"></span> <span class="highlight"></span>
<label>{{i18n "Password"}}</label> <span class="bar"></span>
<div class="error invisible" id="perrorfield"></div> <label>{{i18n "Password"}}</label>
<div class="error invisible" id="perrorfield"></div>
</div>
<button type="button" id="loginbutton" class="mdc-button mdc-button--raised">{{i18n "Login"}}
</button>
</div> </div>
<div id="twofactorgroup">
<ul id="tflist">
</ul>
<button type="button" id="nextbutton" class="mdc-button mdc-button--raised">{{i18n "Next"}} <div id="tfinput">
</button>
<button type="button" id="loginbutton" class="mdc-button mdc-button--raised invisible">{{i18n "Login"}} </div>
</button> </div>
</div> </div>
</form> </form>
<footer> <footer>
<!-- <a href="http://www.polymer-project.org/" target="_blank">
<img src="https://www.polymer-project.org/images/logos/p-logo.svg">
</a> -->
<p>Powered by {{appname}}</p> <p>Powered by {{appname}}</p>
</footer> </footer> --}}
</body> </body>
</html> </html>

View File

@ -55,9 +55,7 @@ nextbutton.onclick = async () => {
}) })
salt = res.salt; salt = res.salt;
usernamegroup.classList.add("invisible") usernamegroup.classList.add("invisible")
nextbutton.classList.add("invisible")
passwordgroup.classList.remove("invisible") passwordgroup.classList.remove("invisible")
loginbutton.classList.remove("invisible")
passwordinput.focus() passwordinput.focus()
} catch (e) { } catch (e) {
showError(uerrorfield, e.message) showError(uerrorfield, e.message)
@ -94,22 +92,26 @@ loginbutton.onclick = async () => {
return data; return data;
}) })
setCookie("login", login.token, login.expires) setCookie("login", login.token, new Date(login.expires).toUTCString());
setCookie("special", special.token, special.expires) setCookie("special", special.token, new Date(special.expires).toUTCString());
let d = new Date() let d = new Date()
d.setTime(d.getTime() + (365 * 24 * 60 * 60 * 1000)); d.setTime(d.getTime() + (30 * 24 * 60 * 60 * 1000)); //Keep the username 30 days
setCookie("username", username, d.toUTCString()); setCookie("username", username, d.toUTCString());
let url = new URL(window.location.href); let url = new URL(window.location.href);
let state = url.searchParams.get("state") let state = url.searchParams.get("state")
let red = "/" let red = "/"
if (state) {
let base64 = url.searchParams.get("base64") if (tfa) twofactor(tfa);
if (base64) else {
red = atob(state) if (state) {
else let base64 = url.searchParams.get("base64")
red = state if (base64)
red = atob(state)
else
red = state
}
window.location.href = red;
} }
window.location.href = red;
} catch (e) { } catch (e) {
passwordinput.value = ""; passwordinput.value = "";
showError(perrorfield, e.message); showError(perrorfield, e.message);
@ -134,4 +136,23 @@ if (username) {
var evt = document.createEvent("HTMLEvents"); var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true); evt.initEvent("change", false, true);
usernameinput.dispatchEvent(evt); usernameinput.dispatchEvent(evt);
}
function twofactor(tfa) {
let list = tfa
.map(entry => {
switch (entry) {
case 0: // OTC
return "Authenticator App";
case 1: // BACKUP
return "Backup Key";
}
return undefined;
})
.filter(e => e !== undefined)
.reduce((p, c) => p + `<li>${c}</li>`, "");
let tfl = document.getElementById("tflist");
tfl.innerHTML = list;
} }

View File

@ -1,10 +1,10 @@
@import "@material/button/mdc-button"; @import "@material/button/mdc-button";
@import "inputs"; @import "inputs";
@import "style"; @import "style";
#loginbutton,
#nextbutton { .spanned-btn {
width: 100%; width: 100%;
background: $primary; // text-shadow: 1px 1px 0 rgba(39, 110, 204, .5); background: $primary !important; // text-shadow: 1px 1px 0 rgba(39, 110, 204, .5);
} }
* { * {
@ -17,7 +17,7 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
hgroup { header {
text-align: center; text-align: center;
margin-top: 4em; margin-top: 4em;
} }

316
views/src/login/login.tsx Normal file
View File

@ -0,0 +1,316 @@
import { h, Component, render } from "preact"
import "inputs"
import "./u2f-api-polyfill"
import sha from "sha512";
import {
setCookie,
getCookie
} from "cookie"
let appname = "test";
function Loader() {
return <div class="loader_box" id="loader">
<div class="loader"></div>
</div>
}
class Username extends Component<{ username: string, onNext: (username: string, salt: string) => void }, { error: string, loading: boolean }> {
username_input: HTMLInputElement;
constructor() {
super();
this.state = { error: undefined, loading: false }
}
async onClick() {
this.setState({ loading: true });
try {
let res = await fetch("/api/user/login?type=username&username=" + this.username_input.value, {
method: "POST"
}).then(e => {
if (e.status !== 200) throw new Error(e.statusText)
return e.json()
}).then(data => {
if (data.error) {
return Promise.reject(new Error(data.error))
}
return data;
})
let salt = res.salt;
this.props.onNext(this.username_input.value, salt);
} catch (err) {
this.setState({
error: err.message
});
}
this.setState({ loading: false });
}
render() {
if (this.state.loading) return <Loader />
return <div>
<div class="floating group">
<input onKeyDown={e => {
let k = e.keyCode | e.which;
if (k === 13) this.onClick();
this.setState({ error: undefined })
}} type="text" value={this.username_input ? this.username_input.value : this.props.username} autofocus ref={elm => elm ? this.username_input = elm : undefined} />
<span class="highlight"></span>
<span class="bar"></span>
<label>Username or Email</label>
{this.state.error ? <div class="error"> {this.state.error}</div> : undefined}
</div>
<button type="button" class="mdc-button mdc-button--raised spanned-btn" onClick={() => this.onClick()}>Next</button>
</div>
}
}
enum TFATypes {
OTC,
BACKUP_CODE,
YUBI_KEY,
APP_ALLOW
}
interface TwoFactors {
id: string;
name: string;
type: TFATypes;
}
class Password extends Component<{ username: string, salt: string, onNext: (login: Token, special: Token, tfa: TwoFactors[]) => void }, { error: string, loading: boolean }> {
password_input: HTMLInputElement;
constructor() {
super();
this.state = { error: undefined, loading: false }
}
async onClick() {
this.setState({
loading: true
});
try {
let pw = sha(this.props.salt + this.password_input.value);
let { login, special, tfa } = await fetch("/api/user/login?type=password", {
method: "POST",
body: JSON.stringify({
username: this.props.username,
password: pw
}),
headers: {
'content-type': 'application/json'
},
}).then(e => {
if (e.status !== 200) throw new Error(e.statusText)
return e.json()
}).then(data => {
if (data.error) {
return Promise.reject(new Error(data.error))
}
return data;
})
this.props.onNext(login, special, tfa);
} catch (err) {
this.setState({ error: err.messagae });
}
this.setState({ loading: false });
}
render() {
if (this.state.loading) return <Loader />
return <div>
<div class="floating group" >
<input onKeyDown={e => {
let k = e.keyCode | e.which;
if (k === 13) this.onClick();
this.setState({ error: undefined })
}} type="password" ref={(elm: HTMLInputElement) => {
if (elm) {
this.password_input = elm
setTimeout(() => elm.focus(), 200)
// elm.focus();
}
}
} />
<span class="highlight"></span>
<span class="bar"></span>
<label>Password</label>
{this.state.error ? <div class="error"> {this.state.error}</div> : undefined}
</div>
<button type="button" class="mdc-button mdc-button--raised spanned-btn" onClick={() => this.onClick()}>Login</button>
</div>
}
}
class TwoFactor extends Component<{ twofactors: TwoFactors[], next: (id: string, type: TFATypes) => void }, {}> {
render() {
let tfs = this.props.twofactors.map(fac => {
let name: string;
switch (fac.type) {
case TFATypes.OTC:
name = "Authenticator"
break;
case TFATypes.BACKUP_CODE:
name = "Backup code";
break;
case TFATypes.APP_ALLOW:
name = "Use App: %s"
break;
case TFATypes.YUBI_KEY:
name = "Use Yubikey: %s"
break;
}
name = name.replace("%s", fac.name ? fac.name : "");
return <li onClick={() => {
console.log("Click on Solution")
this.props.next(fac.id, fac.type)
}}>
{name}
</li>
})
return <div>
<h1>Select one</h1>
<ul>
{tfs}
</ul>
</div>
}
}
// class TFA_YubiKey extends Component<{ id: string, login: Token, special: Token, next: (login: Token, special: Token) => void }, {}> {
// render() {
// }
// }
enum Page {
username,
password,
twofactor,
yubikey
}
interface Token {
token: string;
expires: string;
}
async function apiRequest(endpoint: string, method: "GET" | "POST" | "DELETE" | "PUT" = "GET", body: string = undefined) {
return fetch(endpoint, {
method,
body,
credentials: "same-origin",
headers: {
"content-type": "application/json"
}
}).then(e => {
if (e.status !== 200) throw new Error(e.statusText)
return e.json()
}).then(data => {
if (data.error) {
return Promise.reject(new Error(data.error))
}
return data;
})
}
class App extends Component<{}, { page: Page, username: string, salt: string, twofactor: TwoFactors[], twofactor_id: string }> {
login: Token;
special: Token;
constructor() {
super();
this.state = { page: Page.username, username: getCookie("username"), salt: undefined, twofactor: [], twofactor_id: null }
}
setCookies() {
setCookie("login", this.login.token, new Date(this.login.expires).toUTCString());
setCookie("special", this.special.token, new Date(this.special.expires).toUTCString());
}
finish() {
this.setCookies();
let d = new Date()
d.setTime(d.getTime() + (30 * 24 * 60 * 60 * 1000)); //Keep the username 30 days
setCookie("username", this.state.username, d.toUTCString());
let url = new URL(window.location.href);
let state = url.searchParams.get("state")
let red = "/"
if (state) {
let base64 = url.searchParams.get("base64")
if (base64)
red = atob(state)
else
red = state
}
window.location.href = red;
}
render() {
let cont;
switch (this.state.page) {
case Page.username:
cont = <Username username={this.state.username} onNext={(username, salt) => {
this.setState({ username, salt, page: Page.password })
localStorage.setItem("username", username);
}} />
break;
case Page.password:
cont = <Password username={this.state.username} salt={this.state.salt} onNext={(login, special, twofactor) => {
this.login = login;
this.special = special;
this.setCookies();
if (!twofactor) {
this.finish();
} else {
this.setState({ twofactor, page: Page.twofactor });
}
}} />
break;
case Page.twofactor:
cont = <TwoFactor twofactors={this.state.twofactor} next={async (id, type) => {
if (type === TFATypes.YUBI_KEY) {
let { request } = await apiRequest("/api/user/twofactor/yubikey", "GET");
console.log(request);
(window as any).u2f.sign(request.appId, [request.challenge], [request], async (response) => {
let res = await apiRequest("/api/user/twofactor/yubikey", "PUT", JSON.stringify({ response }));
if (res.success) {
this.login.expires = res.login_exp;
this.special.expires = res.special_exp;
this.finish();
}
})
}
}} />
break;
// case Page.yubikey:
// cont = <TFA_YubiKey id={this.state.twofactor_id} login={this.login} special={this.special} next={(login, special) => {
// this.login = login;
// this.special = special;
// this.finish()
// }} />
// break;
}
return <div>
<header>
<h1>Login</h1>
</header>
<form action="JavaScript:void(0)">
{cont}
</form>
<footer>
<p>Powered by {appname}</p>
</footer>
</div>
}
}
document.addEventListener('DOMContentLoaded', function () {
render(<App />, document.body.querySelector("#content"))
}, false)

View File

@ -0,0 +1,762 @@
//Copyright 2014-2015 Google Inc. All rights reserved.
//Use of this source code is governed by a BSD-style
//license that can be found in the LICENSE file or at
//https://developers.google.com/open-source/licenses/bsd
// NOTE FROM MAINTAINER: This file is copied from google/u2f-ref-code with as
// few alterations as possible. Any changes that were necessary are annotated
// with "NECESSARY CHANGE". These changes, as well as this note, should be
// preserved when updating this file from the source.
/**
* @fileoverview The U2F api.
*/
'use strict';
// NECESSARY CHANGE: wrap the whole file in a closure
(function (){
// NECESSARY CHANGE: detect UA to avoid clobbering other browser's U2F API.
var isChrome = 'chrome' in window && window.navigator.userAgent.indexOf('Edge') < 0;
if ('u2f' in window || !isChrome) {
return;
}
/**
* Namespace for the U2F api.
* @type {Object}
*/
// NECESSARY CHANGE: define the window.u2f API.
var u2f = window.u2f = {};
/**
* FIDO U2F Javascript API Version
* @number
*/
var js_api_version;
/**
* The U2F extension id
* @const {string}
*/
// The Chrome packaged app extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the package Chrome app and does not require installing the U2F Chrome extension.
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
// The U2F Chrome extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the U2F Chrome extension to authenticate.
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
/**
* Message types for messsages to/from the extension
* @const
* @enum {string}
*/
u2f.MessageTypes = {
'U2F_REGISTER_REQUEST': 'u2f_register_request',
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
'U2F_SIGN_REQUEST': 'u2f_sign_request',
'U2F_SIGN_RESPONSE': 'u2f_sign_response',
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
};
/**
* Response status codes
* @const
* @enum {number}
*/
u2f.ErrorCodes = {
'OK': 0,
'OTHER_ERROR': 1,
'BAD_REQUEST': 2,
'CONFIGURATION_UNSUPPORTED': 3,
'DEVICE_INELIGIBLE': 4,
'TIMEOUT': 5
};
/**
* A message for registration requests
* @typedef {{
* type: u2f.MessageTypes,
* appId: ?string,
* timeoutSeconds: ?number,
* requestId: ?number
* }}
*/
u2f.U2fRequest;
/**
* A message for registration responses
* @typedef {{
* type: u2f.MessageTypes,
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
* requestId: ?number
* }}
*/
u2f.U2fResponse;
/**
* An error object for responses
* @typedef {{
* errorCode: u2f.ErrorCodes,
* errorMessage: ?string
* }}
*/
u2f.Error;
/**
* Data object for a single sign request.
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC, USB_INTERNAL}}
*/
u2f.Transport;
/**
* Data object for a single sign request.
* @typedef {Array<u2f.Transport>}
*/
u2f.Transports;
/**
* Data object for a single sign request.
* @typedef {{
* version: string,
* challenge: string,
* keyHandle: string,
* appId: string
* }}
*/
u2f.SignRequest;
/**
* Data object for a sign response.
* @typedef {{
* keyHandle: string,
* signatureData: string,
* clientData: string
* }}
*/
u2f.SignResponse;
/**
* Data object for a registration request.
* @typedef {{
* version: string,
* challenge: string
* }}
*/
u2f.RegisterRequest;
/**
* Data object for a registration response.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: Transports,
* appId: string
* }}
*/
u2f.RegisterResponse;
/**
* Data object for a registered key.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: ?Transports,
* appId: ?string
* }}
*/
u2f.RegisteredKey;
/**
* Data object for a get API register response.
* @typedef {{
* js_api_version: number
* }}
*/
u2f.GetJsApiVersionResponse;
//Low level MessagePort API support
/**
* Sets up a MessagePort to the U2F extension using the
* available mechanisms.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
*/
u2f.getMessagePort = function(callback) {
if (typeof chrome != 'undefined' && chrome.runtime) {
// The actual message here does not matter, but we need to get a reply
// for the callback to run. Thus, send an empty signature request
// in order to get a failure response.
var msg = {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: []
};
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
if (!chrome.runtime.lastError) {
// We are on a whitelisted origin and can talk directly
// with the extension.
u2f.getChromeRuntimePort_(callback);
} else {
// chrome.runtime was available, but we couldn't message
// the extension directly, use iframe
u2f.getIframePort_(callback);
}
});
} else if (u2f.isAndroidChrome_()) {
u2f.getAuthenticatorPort_(callback);
} else if (u2f.isIosChrome_()) {
u2f.getIosPort_(callback);
} else {
// chrome.runtime was not available at all, which is normal
// when this origin doesn't have access to any extensions.
u2f.getIframePort_(callback);
}
};
/**
* Detect chrome running on android based on the browser's useragent.
* @private
*/
u2f.isAndroidChrome_ = function() {
var userAgent = navigator.userAgent;
return userAgent.indexOf('Chrome') != -1 &&
userAgent.indexOf('Android') != -1;
};
/**
* Detect chrome running on iOS based on the browser's platform.
* @private
*/
u2f.isIosChrome_ = function() {
return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1;
};
/**
* Connects directly to the extension via chrome.runtime.connect.
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
* @private
*/
u2f.getChromeRuntimePort_ = function(callback) {
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
{'includeTlsChannelId': true});
setTimeout(function() {
callback(new u2f.WrappedChromeRuntimePort_(port));
}, 0);
};
/**
* Return a 'port' abstraction to the Authenticator app.
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
* @private
*/
u2f.getAuthenticatorPort_ = function(callback) {
setTimeout(function() {
callback(new u2f.WrappedAuthenticatorPort_());
}, 0);
};
/**
* Return a 'port' abstraction to the iOS client app.
* @param {function(u2f.WrappedIosPort_)} callback
* @private
*/
u2f.getIosPort_ = function(callback) {
setTimeout(function() {
callback(new u2f.WrappedIosPort_());
}, 0);
};
/**
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
* @param {Port} port
* @constructor
* @private
*/
u2f.WrappedChromeRuntimePort_ = function(port) {
this.port_ = port;
};
/**
* Format and return a sign request compliant with the JS API version supported by the extension.
* @param {Array<u2f.SignRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatSignRequest_ =
function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: challenge,
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: signRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
appId: appId,
challenge: challenge,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Format and return a register request compliant with the JS API version supported by the extension..
* @param {Array<u2f.SignRequest>} signRequests
* @param {Array<u2f.RegisterRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatRegisterRequest_ =
function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API
for (var i = 0; i < registerRequests.length; i++) {
registerRequests[i].appId = appId;
}
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: registerRequests[0],
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
signRequests: signRequests,
registerRequests: registerRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
appId: appId,
registerRequests: registerRequests,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Posts a message on the underlying channel.
* @param {Object} message
*/
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
this.port_.postMessage(message);
};
/**
* Emulates the HTML 5 addEventListener interface. Works only for the
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
function(eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message' || name == 'onmessage') {
this.port_.onMessage.addListener(function(message) {
// Emulate a minimal MessageEvent object
handler({'data': message});
});
} else {
console.error('WrappedChromeRuntimePort only supports onMessage');
}
};
/**
* Wrap the Authenticator app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedAuthenticatorPort_ = function() {
this.requestId_ = -1;
this.requestObject_ = null;
}
/**
* Launch the Authenticator intent.
* @param {Object} message
*/
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
var intentUrl =
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
';S.request=' + encodeURIComponent(JSON.stringify(message)) +
';end';
document.location = intentUrl;
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
return "WrappedAuthenticatorPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message') {
var self = this;
/* Register a callback to that executes when
* chrome injects the response. */
window.addEventListener(
'message', self.onRequestUpdate_.bind(self, handler), false);
} else {
console.error('WrappedAuthenticatorPort only supports message');
}
};
/**
* Callback invoked when a response is received from the Authenticator.
* @param function({data: Object}) callback
* @param {Object} message message Object
*/
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
function(callback, message) {
var messageObject = JSON.parse(message.data);
var intentUrl = messageObject['intentURL'];
var errorCode = messageObject['errorCode'];
var responseObject = null;
if (messageObject.hasOwnProperty('data')) {
responseObject = /** @type {Object} */ (
JSON.parse(messageObject['data']));
}
callback({'data': responseObject});
};
/**
* Base URL for intents to Authenticator.
* @const
* @private
*/
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
/**
* Wrap the iOS client app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedIosPort_ = function() {};
/**
* Launch the iOS client app request
* @param {Object} message
*/
u2f.WrappedIosPort_.prototype.postMessage = function(message) {
var str = JSON.stringify(message);
var url = "u2f://auth?" + encodeURI(str);
location.replace(url);
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedIosPort_.prototype.getPortType = function() {
return "WrappedIosPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
var name = eventName.toLowerCase();
if (name !== 'message') {
console.error('WrappedIosPort only supports message');
}
};
/**
* Sets up an embedded trampoline iframe, sourced from the extension.
* @param {function(MessagePort)} callback
* @private
*/
u2f.getIframePort_ = function(callback) {
// Create the iframe
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
var iframe = document.createElement('iframe');
iframe.src = iframeOrigin + '/u2f-comms.html';
iframe.setAttribute('style', 'display:none');
document.body.appendChild(iframe);
var channel = new MessageChannel();
var ready = function(message) {
if (message.data == 'ready') {
channel.port1.removeEventListener('message', ready);
callback(channel.port1);
} else {
console.error('First event on iframe port was not "ready"');
}
};
channel.port1.addEventListener('message', ready);
channel.port1.start();
iframe.addEventListener('load', function() {
// Deliver the port to the iframe and initialize
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
});
};
//High-level JS API
/**
* Default extension response timeout in seconds.
* @const
*/
u2f.EXTENSION_TIMEOUT_SEC = 30;
/**
* A singleton instance for a MessagePort to the extension.
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
* @private
*/
u2f.port_ = null;
/**
* Callbacks waiting for a port
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
* @private
*/
u2f.waitingForPort_ = [];
/**
* A counter for requestIds.
* @type {number}
* @private
*/
u2f.reqCounter_ = 0;
/**
* A map from requestIds to client callbacks
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
* |function((u2f.Error|u2f.SignResponse)))>}
* @private
*/
u2f.callbackMap_ = {};
/**
* Creates or retrieves the MessagePort singleton to use.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
* @private
*/
u2f.getPortSingleton_ = function(callback) {
if (u2f.port_) {
callback(u2f.port_);
} else {
if (u2f.waitingForPort_.length == 0) {
u2f.getMessagePort(function(port) {
u2f.port_ = port;
u2f.port_.addEventListener('message',
/** @type {function(Event)} */ (u2f.responseHandler_));
// Careful, here be async callbacks. Maybe.
while (u2f.waitingForPort_.length)
u2f.waitingForPort_.shift()(u2f.port_);
});
}
u2f.waitingForPort_.push(callback);
}
};
/**
* Handles response messages from the extension.
* @param {MessageEvent.<u2f.Response>} message
* @private
*/
u2f.responseHandler_ = function(message) {
var response = message.data;
var reqId = response['requestId'];
if (!reqId || !u2f.callbackMap_[reqId]) {
console.error('Unknown or missing requestId in response.');
return;
}
var cb = u2f.callbackMap_[reqId];
delete u2f.callbackMap_[reqId];
cb(response['responseData']);
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the sign request.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual sign request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
console.log("Extension JS API Version: ", js_api_version);
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual sign request in the supported API version.
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
}
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the register request.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual register request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
console.log("Extension JS API Version: ", js_api_version);
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual register request in the supported API version.
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
}
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatRegisterRequest_(
appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches a message to the extension to find out the supported
* JS API version.
* If the user is on a mobile phone and is thus using Google Authenticator instead
* of the Chrome extension, don't send the request and simply return 0.
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
// If we are using Android Google Authenticator or iOS client app,
// do not fire an intent to ask which JS API version to use.
if (port.getPortType) {
var apiVersion;
switch (port.getPortType()) {
case 'WrappedIosPort_':
case 'WrappedAuthenticatorPort_':
apiVersion = 1.1;
break;
default:
apiVersion = 0;
break;
}
callback({ 'js_api_version': apiVersion });
return;
}
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var req = {
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
requestId: reqId
};
port.postMessage(req);
});
};
})();

18
views/tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"lib": [
"dom",
"es2015",
"es6",
"es7",
"es2018",
"esnext"
],
"jsxFactory": "h",
"jsx": "react",
"module": "esnext"
},
"include": [
"./types.d.ts"
]
}

9
views/types.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare module "sha512" {
const val: any;
export default val;
}
declare module "cookie" {
export function getCookie(name: string): string | undefined;
export function setCookie(name: string, value: string, exp?: string): void;
}