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/
views/out/
views/.*
lib/
keys/
*.old

View File

@ -1,36 +1,37 @@
{
"User not found": "Benutzer nicht gefunden",
"Password or username wrong": "Passwort oder Benutzername falsch",
"Authorize %s": "Authorize %s",
"Login": "Einloggen",
"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",
"Password": "Passwort",
"Next": "Weiter",
"Register": "Registrieren",
"Mail": "Mail",
"Repeat Password": "Passwort wiederholen",
"Username": "Benutzername",
"Name": "Name",
"Registration code": "Registrierungs Schlüssel",
"You need to select one of the options": "Du musst eine der Optionen auswälen",
"Male": "Mann",
"Female": "Frau",
"Other": "Anderes",
"Registration code required": "Registrierungs Schlüssel benötigt",
"Username required": "Benutzername benötigt",
"Name required": "Name benötigt",
"Mail required": "Mail benötigt",
"The passwords do not match": "Die Passwörter stimmen nicht überein",
"Password is required": "Password benötigt",
"Invalid registration code": "Ungültiger Registrierungs Schlüssel",
"Username taken": "Benutzername nicht verfügbar",
"Mail linked with other account": "Mail ist bereits mit einem anderen Account verbunden",
"Registration code already used": "Registrierungs Schlüssel wurde bereits verwendet",
"Administration": "Administration",
"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",
"Client has no permission for acces password auth": "Dieser Client hat keine Berechtigung password auth zu benutzen",
"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."
"User not found": "Benutzer nicht gefunden",
"Password or username wrong": "Passwort oder Benutzername falsch",
"Authorize %s": "Authorize %s",
"Login": "Einloggen",
"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",
"Password": "Passwort",
"Next": "Weiter",
"Register": "Registrieren",
"Mail": "Mail",
"Repeat Password": "Passwort wiederholen",
"Username": "Benutzername",
"Name": "Name",
"Registration code": "Registrierungs Schlüssel",
"You need to select one of the options": "Du musst eine der Optionen auswälen",
"Male": "Mann",
"Female": "Frau",
"Other": "Anderes",
"Registration code required": "Registrierungs Schlüssel benötigt",
"Username required": "Benutzername benötigt",
"Name required": "Name benötigt",
"Mail required": "Mail benötigt",
"The passwords do not match": "Die Passwörter stimmen nicht überein",
"Password is required": "Password benötigt",
"Invalid registration code": "Ungültiger Registrierungs Schlüssel",
"Username taken": "Benutzername nicht verfügbar",
"Mail linked with other account": "Mail ist bereits mit einem anderen Account verbunden",
"Registration code already used": "Registrierungs Schlüssel wurde bereits verwendet",
"Administration": "Administration",
"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",
"Client has no permission for acces password auth": "Dieser Client hat keine Berechtigung password auth zu benutzen",
"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.",
"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/cookie-parser": "^1.4.1",
"@types/dotenv": "^6.1.0",
"@types/express": "^4.16.0",
"@types/handlebars": "^4.0.40",
"@types/i18n": "^0.8.3",
"@types/express": "^4.16.1",
"@types/i18n": "^0.8.5",
"@types/ini": "^1.3.30",
"@types/jsonwebtoken": "^8.3.0",
"@types/mongodb": "^3.1.19",
"@types/node": "^10.12.18",
"@types/node": "^11.9.5",
"@types/node-rsa": "^1.0.0",
"@types/qrcode": "^1.3.1",
"@types/speakeasy": "^2.0.4",
"@types/uuid": "^3.4.4",
"concurrently": "^4.1.0",
"nodemon": "^1.18.9",
"typescript": "^3.2.4"
"nodemon": "^1.18.10",
"typescript": "^3.3.3333"
},
"dependencies": {
"@hibas123/nodelogging": "^1.3.21",
"@hibas123/nodeloggingserver_client": "^1.1.2",
"@hibas123/safe_mongo": "^1.4.5",
"@hibas123/safe_mongo": "^1.5.3",
"body-parser": "^1.18.3",
"compression": "^1.7.3",
"cookie-parser": "^1.4.3",
"cookie-parser": "^1.4.4",
"cors": "^2.8.5",
"dotenv": "^6.2.0",
"express": "^4.16.4",
"handlebars": "^4.0.12",
"handlebars": "^4.1.0",
"i18n": "^0.8.3",
"ini": "^1.3.5",
"jsonwebtoken": "^8.4.0",
"moment": "^2.23.0",
"mongodb": "^3.1.12",
"node-rsa": "^1.0.2",
"jsonwebtoken": "^8.5.0",
"moment": "^2.24.0",
"mongodb": "^3.1.13",
"node-rsa": "^1.0.3",
"qrcode": "^1.3.3",
"reflect-metadata": "^0.1.13",
"speakeasy": "^2.0.0",
"u2f": "^0.1.3",
"uuid": "^3.3.2"
}
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
import { Request, Response, NextFunction, RequestHandler } from "express";
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) => {
let p = handler(req, res, (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) => {
let hc = handler.concat();
while (hc.length > 0) {

View File

@ -1,5 +1,5 @@
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 RequestError, { HttpStatusCode } from "../../helper/request_error";
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 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) {
const invalid = () => {
throw new Invalid();
@ -24,8 +24,7 @@ export function GetUserMiddleware(json = false, special_token: boolean = false,
if (!login) invalid()
let token = await LoginToken.findOne({ token: login, valid: true })
if (!token) invalid()
if (!token.validated) invalid();
if (!await CheckToken(token, validated)) invalid();
let user = await User.findById(token.user);
if (!user) {
@ -34,31 +33,23 @@ export function GetUserMiddleware(json = false, special_token: boolean = false,
invalid();
}
if (token.validTill.getTime() < new Date().getTime()) { //Token expired
token.valid = false;
await LoginToken.save(token);
invalid()
}
let special_token;
if (special) {
Logging.debug("Special found")
let st = await LoginToken.findOne({ token: special, special: true, valid: true })
if (st && st.validated && st.valid && st.user.toHexString() === token.user.toHexString()) {
if (st.validTill.getTime() < new Date().getTime()) { //Token expired
Logging.debug("Special expired")
st.valid = false;
await LoginToken.save(st);
} else {
Logging.debug("Special valid")
req.special = true;
}
}
special_token = await LoginToken.findOne({ token: special, special: true, valid: true, user: token.user })
if (!await CheckToken(special_token, validated))
invalid();
req.special = true;
}
if (special_token && !req.special) invalid();
if (special_required && !req.special) invalid();
req.user = user
req.isAdmin = user.admin;
req.token = {
login: token,
special: special_token
}
if (next)
next()
@ -67,7 +58,7 @@ export function GetUserMiddleware(json = false, special_token: boolean = false,
if (e instanceof Invalid) {
if (req.method === "GET" && !json) {
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 {
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 AuthRoute from "./oauth/auth";
import JWTRoute from "./oauth/jwt";
import Public from "./oauth/public";
import RefreshTokenRoute from "./oauth/refresh";
import AuthRoute from "./auth";
import JWTRoute from "./jwt";
import Public from "./public";
import RefreshTokenRoute from "./refresh";
const OAuthRoue: Router = Router();
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 User, { IUser, TokenTypes } from "../../models/user";
import User, { IUser } from "../../models/user";
import { randomBytes } from "crypto";
import moment = require("moment");
import LoginToken from "../../models/login_token";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import promiseMiddleware from "../../helper/promiseMiddleware";
import * as speakeasy from "speakeasy";
import TwoFactor from "../../models/twofactor";
const Login = promiseMiddleware(async (req: Request, res: Response) => {
let type = req.query.type;
@ -19,7 +21,13 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
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 tfa_exp = moment().add(5, "minutes").toDate()
let token_exp = moment().add(6, "months").toDate()
@ -28,7 +36,8 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
valid: true,
validTill: tfa ? tfa_exp : token_exp,
user: user._id,
validated: tfa ? false : true
validated: tfa ? false : true,
...client
});
await LoginToken.save(token);
@ -40,7 +49,8 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
validTill: tfa ? tfa_exp : special_exp,
special: true,
user: user._id,
validated: tfa ? false : true
validated: tfa ? false : true,
...client
});
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 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) {
res.json({ error: req.__("Password or username wrong") })
} 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 {
if (user.twofactor && user.twofactor.length > 0) {
let types = user.twofactor.filter(f => f.valid).map(f => f.type)
await sendToken(user, types);
} else {
await sendToken(user);
}
await sendToken(user);
}
}
}
} 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 {
name: string
url: string
dev: string
}
export interface Config {
@ -31,11 +33,12 @@ import * as dotenv from "dotenv";
import { Logging } from "@hibas123/nodelogging";
dotenv.config();
const config: Config = ini.parse(readFileSync("./config.ini").toString())
if (config.dev) config.dev = Boolean(config.dev);
if (process.env.DEV === "true") {
const config = ini.parse(readFileSync("./config.ini").toString()) as Config;
if (config.core.dev) config.dev = Boolean(config.core.dev);
if (process.env.DEV === "true")
config.dev = true;
if (config.dev)
Logging.warning("DEV mode active. This can cause major performance issues, data loss and vulnerabilities! ")
}
export default config;

5
src/express.d.ts vendored
View File

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

View File

@ -1,6 +1,7 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
import moment = require("moment");
export interface ILoginToken extends ModelDataBase {
token: string;
@ -9,6 +10,9 @@ export interface ILoginToken extends ModelDataBase {
validTill: Date;
valid: boolean;
validated: boolean;
data: any;
ip: string;
browser: string;
}
const LoginToken = DB.addModel<ILoginToken>({
name: "login_token",
@ -31,7 +35,31 @@ const LoginToken = DB.addModel<ILoginToken>({
valid: { type: Boolean },
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;

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
}
export enum TokenTypes {
OTC,
BACKUP_CODE
}
export interface IUser extends ModelDataBase {
uid: string;
username: string;
@ -28,7 +23,6 @@ export interface IUser extends ModelDataBase {
salt: string;
mails: ObjectID[];
phones: { phone: string, verified: boolean, primary: boolean }[];
twofactor: { token: string, valid: boolean, type: TokenTypes }[];
encryption_key: string;
}
@ -100,6 +94,33 @@ const User = DB.addModel<IUser>({
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 { ObjectID } from "bson";
import DB from "./database";
import TwoFactor from "./models/twofactor";
export default async function TestData() {
await DB.db.dropDatabase();
@ -61,4 +62,19 @@ export default async function TestData() {
})
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();
template = handlebars.compile(html);
}
/**
* Benchmarks (5000, 500 cuncurrent)
* Plain:
@ -26,7 +27,6 @@ function loadStatic() {
* - prod 6sec
*/
export default function GetLoginPage(__: typeof i__): string {
if (config.dev) {
loadStatic()

View File

@ -13,10 +13,11 @@ import Client from "../models/client";
import { Logging } from "@hibas123/nodelogging";
import Stacker from "../api/middlewares/stacker";
import { UserMiddleware, GetUserMiddleware } from "../api/middlewares/user";
import GetUserPage from "./user";
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();
ViewRouter.get("/", UserMiddleware, (req, res) => {
@ -40,6 +41,11 @@ ViewRouter.get("/admin", GetUserMiddleware(false, true), (req: Request, res, nex
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) => {
let { scope, redirect_uri, state, client_id }: { [key: string]: string } = req.query;
const sendError = (type) => {
@ -85,7 +91,7 @@ if (config.dev) {
res.send(GetAuthPage(req.__, "Test 05265", [
{
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
},
{

View File

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

View File

@ -4,13 +4,16 @@ const {
mkdirSync,
copyFileSync,
writeFileSync,
readFileSync
readFileSync,
exists
} = require('fs')
const {
join,
basename
basename,
dirname
} = require('path')
const includepaths = require("rollup-plugin-includepaths")
const isDirectory = source => lstatSync(source).isDirectory()
const getDirectories = source =>
@ -24,6 +27,8 @@ function ensureDir(folder) {
}
}
const fileExists = (filename) => new Promise((yes, no) => exists(filename, (exi) => yes(exi)));
ensureDir("./out")
const sass = require('sass');
@ -38,29 +43,77 @@ function findHead(elm) {
}
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 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 outpath = "./out/" + pagename;
ensureDir(outpath)
const basefile = await file_name(folder, pagename, ["tsx", "ts", "js"]);
let bundle = await rollup.rollup({
input: `${folder}/${pagename}.js`,
plugins: [includepaths({
paths: ["shared"]
})],
input: basefile,
plugins: [
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 {
code,
map
} = await bundle.generate({
let { output } = await bundle.generate({
format: "iife",
compact: true
})
let { code } = output[0];
let sass_res = sass.renderSync({
file: folder + `/${pagename}.scss`,
@ -94,15 +147,14 @@ async function buildPage(folder, name) {
collapseWhitespace: true,
html5: true,
keepClosingSlash: true,
minifyCSS: true,
minifyJS: true,
minifyCSS: false,
minifyJS: false,
removeComments: true,
useShortDoctype: true
})
let gzips = await gzipSize(result)
writeFileSync(`${outpath}/${pagename}.html`, result)
let stats = {
sass: sass_res.stats,
js: {
@ -119,42 +171,24 @@ async function buildPage(folder, name) {
}
async function run() {
const pages = getDirectories("./src");
const ProgressBar = require('progress');
// const bar = new ProgressBar('[:bar] :current/:total :percent :elapseds :etas', {
// // schema: '[:bar] :current/:total :percent :elapseds :etas',
// total: pages.length
// });
console.log("Start compiling!");
let pages = getDirectories("./src");
await Promise.all(pages.map(async e => {
try {
await buildPage(e)
} catch (er) {
console.error("Failed compiling", basename(e))
console.log(er.message)
console.log(er)
}
// bar.tick()
}))
console.log("Finished compiling!")
}
if (process.argv.join(" ").toLowerCase().indexOf("watch") < 0) {
run()
} else {
const nodemon = require('nodemon');
nodemon({
script: "dummy.js",
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);
});
}
const chokidar = require("chokidar");
if (process.argv.join(" ").toLowerCase().indexOf("watch") >= 0)
chokidar.watch(["./src", "./node_modules", "./package.json", "./package-lock.json"], {
ignoreInitial: true
})
.on("all", () => run());
run()

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"
},
"dependencies": {
"@material/button": "^0.41.0",
"@material/form-field": "^0.41.0",
"@material/radio": "^0.41.0",
"ascii-progress": "^1.0.5",
"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"
"@material/button": "^0.44.1",
"@material/form-field": "^0.44.1",
"@material/radio": "^0.44.1",
"preact": "^8.4.2"
},
"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() {
if (e.value !== "") {
if (e.classList.contains("used")) return;
e.classList.add("used")
} else {
if (e.classList.contains("used")) e.classList.remove("used")
}
(() => {
const run = () => {
document.querySelectorAll(".floating>input").forEach(e => {
function checkState() {
if (e.value !== "") {
if (e.classList.contains("used")) return;
e.classList.add("used")
} else {
if (e.classList.contains("used")) e.classList.remove("used")
}
}
e.addEventListener("change", () => checkState())
checkState()
})
}
e.addEventListener("change", () => checkState())
checkState()
})
run();
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>
{{#scopes}}
<li>
<div style="display:inline-block">
<div class="permission">
{{#if logo}}
<div class="image">
<img width="50px" height="50px" src="{{logo}}">
</div>
{{!-- <div class="image"> --}}
<img class="image" src="{{logo}}">
{{!-- </div> --}}
{{/if}}
<div class="text">
<h3 class="scope_title">{{name}}</h3>

View File

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

View File

@ -7,7 +7,8 @@
</head>
<body>
<hgroup>
<div id="content"></div>
{{!-- <hgroup>
<h1>{{i18n "Login"}}</h1>
</hgroup>
<form action="JavaScript:void(0)">
@ -15,33 +16,41 @@
<div class="loader"></div>
</div>
<div id="container">
<div class="floating group" id="usernamegroup">
<input type="text" id="username" autofocus>
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Username or Email"}}</label>
<div class="error invisible" id="uerrorfield"></div>
<div id="usernamegroup">
<div class="floating group">
<input type="text" id="username" autofocus>
<span class="highlight"></span>
<span class="bar"></span>
<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 class="floating group invisible" id="passwordgroup">
<input type="password" id="password">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Password"}}</label>
<div class="error invisible" id="perrorfield"></div>
<div id="passwordgroup">
<div class="floating group invisible" id="passwordgroup">
<input type="password" id="password">
<span class="highlight"></span>
<span class="bar"></span>
<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 id="twofactorgroup">
<ul id="tflist">
</ul>
<button type="button" id="nextbutton" class="mdc-button mdc-button--raised">{{i18n "Next"}}
</button>
<button type="button" id="loginbutton" class="mdc-button mdc-button--raised invisible">{{i18n "Login"}}
</button>
<div id="tfinput">
</div>
</div>
</div>
</form>
<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>
</footer>
</footer> --}}
</body>
</html>

View File

@ -55,9 +55,7 @@ nextbutton.onclick = async () => {
})
salt = res.salt;
usernamegroup.classList.add("invisible")
nextbutton.classList.add("invisible")
passwordgroup.classList.remove("invisible")
loginbutton.classList.remove("invisible")
passwordinput.focus()
} catch (e) {
showError(uerrorfield, e.message)
@ -94,22 +92,26 @@ loginbutton.onclick = async () => {
return data;
})
setCookie("login", login.token, login.expires)
setCookie("special", special.token, special.expires)
setCookie("login", login.token, new Date(login.expires).toUTCString());
setCookie("special", special.token, new Date(special.expires).toUTCString());
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());
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
if (tfa) twofactor(tfa);
else {
if (state) {
let base64 = url.searchParams.get("base64")
if (base64)
red = atob(state)
else
red = state
}
window.location.href = red;
}
window.location.href = red;
} catch (e) {
passwordinput.value = "";
showError(perrorfield, e.message);
@ -134,4 +136,23 @@ if (username) {
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
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 "inputs";
@import "style";
#loginbutton,
#nextbutton {
.spanned-btn {
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;
}
hgroup {
header {
text-align: center;
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;
}