Added U2F Support for YubiKey
This commit is contained in:
parent
aa47e6c92f
commit
c54406564c
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
node_modules/
|
||||
views/out/
|
||||
views/.*
|
||||
lib/
|
||||
keys/
|
||||
*.old
|
||||
|
@ -32,5 +32,6 @@
|
||||
"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."
|
||||
"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
429
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
@ -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)
|
@ -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);
|
@ -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) {
|
||||
|
@ -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")
|
||||
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)
|
||||
}
|
||||
|
@ -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);
|
@ -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
14
src/api/user/index.ts
Normal 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;
|
@ -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") {
|
||||
|
||||
} else {
|
||||
if (user.twofactor && user.twofactor.length > 0) {
|
||||
let types = user.twofactor.filter(f => f.valid).map(f => f.type)
|
||||
await sendToken(user, types);
|
||||
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 {
|
||||
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
29
src/api/user/token.ts
Normal 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 });
|
||||
});
|
79
src/api/user/twofactor/helper.ts
Normal file
79
src/api/user/twofactor/helper.ts
Normal 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;
|
||||
}
|
43
src/api/user/twofactor/index.ts
Normal file
43
src/api/user/twofactor/index.ts
Normal 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;
|
131
src/api/user/twofactor/yubikey/index.ts
Normal file
131
src/api/user/twofactor/yubikey/index.ts
Normal 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;
|
@ -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
5
src/express.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
57
src/models/twofactor.ts
Normal 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;
|
@ -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)
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
},
|
||||
{
|
||||
|
10
src/web.ts
10
src/web.ts
@ -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) {
|
||||
this.server.use(compression({
|
||||
filter: (req, res) => {
|
||||
if (req.headers['x-no-compression']) {
|
||||
// don't compress responses with this request header
|
||||
return false
|
||||
}
|
||||
|
||||
// fallback to standard filter function
|
||||
return compression.filter(req, res)
|
||||
}
|
||||
this.server.use(compression({ filter: shouldCompress }))
|
||||
}));
|
||||
}
|
||||
|
||||
private registerEndpoints() {
|
||||
|
122
views/build.js
122
views/build.js
@ -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()
|
||||
|
2393
views/package-lock.json
generated
2393
views/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
document.querySelectorAll(".floating>input").forEach(e => {
|
||||
(() => {
|
||||
const run = () => {
|
||||
document.querySelectorAll(".floating>input").forEach(e => {
|
||||
function checkState() {
|
||||
if (e.value !== "") {
|
||||
if (e.classList.contains("used")) return;
|
||||
@ -10,4 +12,22 @@ document.querySelectorAll(".floating>input").forEach(e => {
|
||||
|
||||
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
2
views/shared/preact.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||
|
@ -34,20 +34,27 @@ ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
.permission {
|
||||
display: flex;
|
||||
img {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
float: left;
|
||||
}
|
||||
.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;
|
||||
|
@ -7,7 +7,8 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<hgroup>
|
||||
<div id="content"></div>
|
||||
{{!-- <hgroup>
|
||||
<h1>{{i18n "Login"}}</h1>
|
||||
</hgroup>
|
||||
<form action="JavaScript:void(0)">
|
||||
@ -15,13 +16,18 @@
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
<div id="container">
|
||||
<div class="floating group" id="usernamegroup">
|
||||
<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 id="passwordgroup">
|
||||
<div class="floating group invisible" id="passwordgroup">
|
||||
<input type="password" id="password">
|
||||
<span class="highlight"></span>
|
||||
@ -29,19 +35,22 @@
|
||||
<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>
|
@ -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,14 +92,17 @@ 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 (tfa) twofactor(tfa);
|
||||
else {
|
||||
if (state) {
|
||||
let base64 = url.searchParams.get("base64")
|
||||
if (base64)
|
||||
@ -110,6 +111,7 @@ loginbutton.onclick = async () => {
|
||||
red = state
|
||||
}
|
||||
window.location.href = red;
|
||||
}
|
||||
} catch (e) {
|
||||
passwordinput.value = "";
|
||||
showError(perrorfield, e.message);
|
||||
@ -135,3 +137,22 @@ if (username) {
|
||||
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;
|
||||
}
|
@ -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
316
views/src/login/login.tsx
Normal 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)
|
762
views/src/login/u2f-api-polyfill.js
Normal file
762
views/src/login/u2f-api-polyfill.js
Normal 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
18
views/tsconfig.json
Normal 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
9
views/types.d.ts
vendored
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user