Improve popup window support.
All checks were successful
continuous-integration/drone/push Build is passing

Switching to new views_repo with new build system
This commit is contained in:
Fabian Stamm 2020-11-03 23:29:56 +01:00
parent 6b4ad81940
commit dd10cae1cd
19 changed files with 163 additions and 1800 deletions

View File

@ -16,6 +16,7 @@ import moment = require("moment");
// import { JWTExpDur } from "../../keys"; // import { JWTExpDur } from "../../keys";
import RefreshToken from "../../models/refresh_token"; import RefreshToken from "../../models/refresh_token";
import { getEncryptionKey } from "../../helper/user_key"; import { getEncryptionKey } from "../../helper/user_key";
import { refreshTokenValidTime } from "../../config";
// TODO: // TODO:
/* /*
@ -27,8 +28,6 @@ legitimate client, one of them will present an invalidated refresh
token, which will inform the authorization server of the breach. token, which will inform the authorization server of the breach.
*/ */
const refreshTokenValidTime = moment.duration(6, "month");
const RefreshTokenRoute = Stacker( const RefreshTokenRoute = Stacker(
GetClientAuthMiddleware(false, false, true), GetClientAuthMiddleware(false, false, true),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {

View File

@ -1,11 +1,11 @@
import { Router } from "express"; import { Router } from "express";
import { GetAccount } from "./account"; import { GetAccount } from "./account";
import { GetContactInfos } from "./contact"; import { GetContactInfos } from "./contact";
import { GetJWTByUser } from "./jwt";
import Login from "./login"; import Login from "./login";
import Register from "./register"; import Register from "./register";
import { DeleteToken, GetToken } from "./token"; import { DeleteToken, GetToken } from "./token";
import TwoFactorRoute from "./twofactor"; import TwoFactorRoute from "./twofactor";
import OAuthRoute from "./oauth";
const UserRoute: Router = Router(); const UserRoute: Router = Router();
@ -127,6 +127,6 @@ UserRoute.get("/account", GetAccount);
*/ */
UserRoute.get("/contact", GetContactInfos); UserRoute.get("/contact", GetContactInfos);
UserRoute.get("/jwt", GetJWTByUser); UserRoute.use("/oauth", OAuthRoute);
export default UserRoute; export default UserRoute;

View File

@ -1,37 +0,0 @@
import { Request, Response } from "express";
import Stacker from "../middlewares/stacker";
import { GetUserMiddleware } from "../middlewares/user";
import { URL } from "url";
import Client from "../../models/client";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import { getAccessTokenJWT } from "../../helper/jwt";
export const GetJWTByUser = Stacker(
GetUserMiddleware(true, false),
async (req: Request, res: Response) => {
const { client_id, origin } = req.query as { [key: string]: string };
const client = await Client.findOne({
client_id,
});
const clientNotFoundError = new RequestError(
"Client not found!",
HttpStatusCode.BAD_REQUEST
);
if (!client) throw clientNotFoundError;
const clientUrl = new URL(client.redirect_url);
if (clientUrl.hostname !== origin) throw clientNotFoundError;
const jwt = await getAccessTokenJWT({
user: req.user,
client: client,
permissions: [],
});
res.json({ jwt });
}
);

View File

@ -0,0 +1,21 @@
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import Client, { IClient } from "../../../models/client";
export async function getClientWithOrigin(client_id: string, origin: string) {
const client = await Client.findOne({
client_id,
});
const clientNotFoundError = new RequestError(
"Client not found!",
HttpStatusCode.BAD_REQUEST
);
if (!client) throw clientNotFoundError;
const clientUrl = new URL(client.redirect_url);
if (clientUrl.hostname !== origin) throw clientNotFoundError;
return client;
}

View File

@ -0,0 +1,12 @@
import { Router } from "express";
import { GetJWTByUser } from "./jwt";
import { GetPermissionsForAuthRequest } from "./permissions";
import { GetTokenByUser } from "./refresh_token";
const router = Router();
router.get("/jwt", GetJWTByUser);
router.get("/permissions", GetPermissionsForAuthRequest);
router.get("/refresh_token", GetTokenByUser);
export default router;

25
src/api/user/oauth/jwt.ts Normal file
View File

@ -0,0 +1,25 @@
import { Request, Response } from "express";
import Stacker from "../../middlewares/stacker";
import { GetUserMiddleware } from "../../middlewares/user";
import { URL } from "url";
import Client from "../../../models/client";
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import { getAccessTokenJWT } from "../../../helper/jwt";
import { getClientWithOrigin } from "./_helper";
export const GetJWTByUser = Stacker(
GetUserMiddleware(true, false),
async (req: Request, res: Response) => {
const { client_id, origin } = req.query as { [key: string]: string };
const client = await getClientWithOrigin(client_id, origin);
const jwt = await getAccessTokenJWT({
user: req.user,
client: client,
permissions: [],
});
res.json({ jwt });
}
);

View File

@ -0,0 +1,38 @@
import { Request, Response } from "express";
import Stacker from "../../middlewares/stacker";
import { GetUserMiddleware } from "../../middlewares/user";
import { URL } from "url";
import Client from "../../../models/client";
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import { randomBytes } from "crypto";
import moment = require("moment");
import RefreshToken from "../../../models/refresh_token";
import { refreshTokenValidTime } from "../../../config";
import { getClientWithOrigin } from "./_helper";
import Permission from "../../../models/permissions";
export const GetPermissionsForAuthRequest = Stacker(
GetUserMiddleware(true, false),
async (req: Request, res: Response) => {
const { client_id, origin, permissions } = req.query as {
[key: string]: string;
};
const client = await getClientWithOrigin(client_id, origin);
const perm = permissions.split(",").filter((e) => !!e);
const resolved = await Promise.all(
perm.map((p) => Permission.findById(p))
);
if (resolved.some((e) => e.grant_type !== "user")) {
throw new RequestError(
"Invalid Permission requested",
HttpStatusCode.BAD_REQUEST
);
}
res.json({ permissions: resolved });
}
);

View File

@ -0,0 +1,49 @@
import { Request, Response } from "express";
import Stacker from "../../middlewares/stacker";
import { GetUserMiddleware } from "../../middlewares/user";
import { URL } from "url";
import Client from "../../../models/client";
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import { randomBytes } from "crypto";
import moment = require("moment");
import RefreshToken from "../../../models/refresh_token";
import { refreshTokenValidTime } from "../../../config";
import { getClientWithOrigin } from "./_helper";
import Permission from "../../../models/permissions";
export const GetTokenByUser = Stacker(
GetUserMiddleware(true, false),
async (req: Request, res: Response) => {
const { client_id, origin, permissions } = req.query as {
[key: string]: string;
};
const client = await getClientWithOrigin(client_id, origin);
const perm = permissions.split(",").filter((e) => !!e);
const resolved = await Promise.all(
perm.map((p) => Permission.findById(p))
);
if (resolved.some((e) => e.grant_type !== "user")) {
throw new RequestError(
"Invalid Permission requested",
HttpStatusCode.BAD_REQUEST
);
}
let token = RefreshToken.new({
user: req.user._id,
client: client._id,
permissions: resolved.map((e) => e._id),
token: randomBytes(16).toString("hex"),
valid: true,
validTill: moment().add(refreshTokenValidTime).toDate(),
});
await RefreshToken.save(token);
res.json({ token });
}
);

View File

@ -1,8 +1,9 @@
import { parse } from "@hibas123/config"; import { parse } from "@hibas123/config";
import { Logging } from "@hibas123/nodelogging"; import { Logging } from "@hibas123/nodelogging";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { readFileSync } from "fs"; import moment = require("moment");
import * as ini from "ini";
export const refreshTokenValidTime = moment.duration(6, "month");
dotenv.config(); dotenv.config();

View File

@ -40,13 +40,13 @@ ViewRouter.get("/register", (req, res) => {
ViewRouter.use( ViewRouter.use(
"/login", "/login",
addCache, addCache,
ServeStatic("./views_repo/build/login", { cacheControl: false }) ServeStatic("./views_repo/build/Login", { cacheControl: false })
); );
ViewRouter.use( ViewRouter.use(
"/user", "/user",
addCache, addCache,
ServeStatic("./views_repo/build/user", { cacheControl: false }) ServeStatic("./views_repo/build/User", { cacheControl: false })
); );
ViewRouter.get("/code", (req, res) => { ViewRouter.get("/code", (req, res) => {
@ -69,9 +69,15 @@ ViewRouter.get(
ViewRouter.get("/auth", GetAuthRoute(true)); ViewRouter.get("/auth", GetAuthRoute(true));
ViewRouter.get("/popup", UserMiddleware, (req, res) => { ViewRouter.use(
res.send(GetPopupPage(req.__)); "/popup",
}); addCache,
ServeStatic("./views_repo/build/Popup", { cacheControl: false })
);
// ViewRouter.get("/popup", UserMiddleware, (req, res) => {
// res.send(GetPopupPage(req.__));
// });
// if (config.core.dev) { // if (config.core.dev) {
// const logo = // const logo =

View File

@ -1,56 +0,0 @@
<html>
<head>
<title>{{i18n "Login"}}</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
<div id="content"></div>
{{!-- <hgroup>
<h1>{{i18n "Login"}}</h1>
</hgroup>
<form action="JavaScript:void(0)">
<div class="loader_box" id="loader">
<div class="loader"></div>
</div>
<div id="container">
<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>
<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>
<div id="tfinput">
</div>
</div>
</div>
</form>
<footer>
<p>Powered by {{appname}}</p>
</footer> --}}
</body>
</html>

View File

@ -1,166 +0,0 @@
import sha from "sha512";
import { setCookie, getCookie } from "cookie";
import "inputs";
const loader = document.getElementById("loader");
const container = document.getElementById("container");
const usernameinput = document.getElementById("username");
const usernamegroup = document.getElementById("usernamegroup");
const uerrorfield = document.getElementById("uerrorfield");
const passwordinput = document.getElementById("password");
const passwordgroup = document.getElementById("passwordgroup");
const perrorfield = document.getElementById("perrorfield");
const nextbutton = document.getElementById("nextbutton");
const loginbutton = document.getElementById("loginbutton");
let username;
let salt;
usernameinput.focus();
const loading = () => {
container.style.filter = "blur(2px)";
loader.style.display = "";
};
const loading_fin = () => {
container.style.filter = "";
loader.style.display = "none";
};
loading_fin();
usernameinput.onkeydown = (e) => {
var keycode = e.keyCode ? e.keyCode : e.which;
if (keycode === 13) nextbutton.click();
clearError(uerrorfield);
};
nextbutton.onclick = async () => {
loading();
username = usernameinput.value;
try {
let res = await fetch(
"/api/user/login?type=username&username=" + username,
{
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;
});
salt = res.salt;
usernamegroup.classList.add("invisible");
passwordgroup.classList.remove("invisible");
passwordinput.focus();
} catch (e) {
showError(uerrorfield, e.message);
}
loading_fin();
};
passwordinput.onkeydown = (e) => {
var keycode = e.keyCode ? e.keyCode : e.which;
if (keycode === 13) loginbutton.click();
clearError(perrorfield);
};
loginbutton.onclick = async () => {
loading();
let pw = sha(salt + passwordinput.value);
try {
let { login, special, tfa } = await fetch(
"/api/user/login?type=password",
{
method: "POST",
body: JSON.stringify({
username: usernameinput.value,
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;
});
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() + 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) red = atob(state);
else red = state;
}
window.location.href = red;
}
} catch (e) {
passwordinput.value = "";
showError(perrorfield, e.message);
}
loading_fin();
};
function clearError(field) {
field.innerText = "";
field.classList.add("invisible");
}
function showError(field, error) {
field.innerText = error;
field.classList.remove("invisible");
}
username = getCookie("username");
if (username) {
usernameinput.value = 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,132 +0,0 @@
@import "@material/button/mdc-button";
@import "inputs";
@import "style";
.spanned-btn {
width: 100%;
background: $primary !important; // text-shadow: 1px 1px 0 rgba(39, 110, 204, .5);
}
* {
box-sizing: border-box;
}
body {
font-family: Helvetica;
background: #eee;
-webkit-font-smoothing: antialiased;
}
header {
text-align: center;
margin-top: 4em;
}
h1,
h3 {
font-weight: 300;
}
h1 {
color: #636363;
}
h3 {
color: $primary;
}
form {
max-width: 380px;
margin: 4em auto;
padding: 3em 2em 2em 2em;
background: #fafafa;
border: 1px solid #ebebeb;
box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px,
rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px;
position: relative;
}
.loader_box {
width: 64px;
height: 64px;
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.loader {
display: inline-block;
position: relative;
z-index: 100;
}
.loader:after {
content: " ";
display: block;
width: 46px;
height: 46px;
margin: 1px;
border-radius: 50%;
border: 5px solid #000000;
border-color: #000000 transparent #000000 transparent;
animation: loader 1.2s linear infinite;
}
@keyframes loader {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
footer {
text-align: center;
}
footer p {
color: #888;
font-size: 13px;
letter-spacing: 0.4px;
}
footer a {
color: $primary;
text-decoration: none;
transition: all 0.2s ease;
}
footer a:hover {
color: #666;
text-decoration: underline;
}
footer img {
width: 80px;
transition: all 0.2s ease;
}
footer img:hover {
opacity: 0.83;
}
footer img:focus,
footer a:focus {
outline: none;
}
.invisible {
display: none;
}
.errorColor {
background: $error !important;
}
.error {
color: $error;
margin-top: 5px;
font-size: 13px;
}

View File

@ -1,434 +0,0 @@
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

@ -1,844 +0,0 @@
//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);
});
};
})();

View File

@ -1,24 +0,0 @@
<html>
<head>
<title>Popup Auth</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
<div class="card">
<div class="title">
<h1>Allow <span id="hostname" style="font-weight: bold;">LOADING...</span> to authenticate you?</h1>
</div>
<div>
<div style="text-align: right;">
<button onclick="allow()" class="mdc-button mdc-button--raised blue-button">Allow</button>
<button onclick="deny()" class="mdc-button mdc-button--raised blue-button">Deny</button>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,32 +0,0 @@
async function getJWT(client_id, hostname) {
hostname = encodeURIComponent(hostname);
client_id = encodeURIComponent(client_id);
const res = await fetch(
`/api/user/jwt?client_id=${client_id}&origin=${hostname}`
).then((res) => res.json());
return res;
}
let acceptPromise;
window.allow = () => acceptPromise();
window.deny = () => window.close();
function start() {
let started = false;
window.addEventListener("message", async (msg) => {
if (!started) {
started = true;
const url = new URL(msg.origin);
document.getElementById("hostname").innerText = url.hostname;
await new Promise((yes) => (acceptPromise = yes));
const res = await getJWT(msg.data.client_id, url.hostname);
msg.source.postMessage(res, msg.origin);
window.close();
}
});
}
start();

View File

@ -1,63 +0,0 @@
@import "@material/button/mdc-button";
.blue-button {
background: #4a89dc !important;
}
.grey-button {
background: #797979 !important;
}
hr {
// display: block;
// height: 1px;
border: 0;
border-top: 1px solid #b8b8b8;
// margin: 1em 0;
// padding: 0;
}
body {
font-family: Helvetica;
background: #eee;
-webkit-font-smoothing: antialiased;
}
.title {
text-align: center;
}
h1,
h3 {
font-weight: 300;
}
h1 {
color: #636363;
}
ul {
list-style: none;
padding-left: 0;
}
.scope_title {
margin-top: 0;
margin-bottom: 0;
padding-left: 5px;
}
.scope_description {
margin-top: 0;
padding-left: 15px;
font-size: 13px;
color: #202020;
}
.card {
max-width: 480px;
margin: 4em auto;
padding: 3em 2em 2em 2em;
background: #fafafa;
border: 1px solid #ebebeb;
box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px,
rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px;
}

@ -1 +1 @@
Subproject commit 4191522b24334f20f7dcda811ca43959b02fef7a Subproject commit 484be5a04879ec78c9ab902c92fc166d80dc7f41