Added U2F Support for YubiKey
This commit is contained in:
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()
|
||||
|
2395
views/package-lock.json
generated
2395
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,13 +1,33 @@
|
||||
document.querySelectorAll(".floating>input").forEach(e => {
|
||||
function checkState() {
|
||||
if (e.value !== "") {
|
||||
if (e.classList.contains("used")) return;
|
||||
e.classList.add("used")
|
||||
} else {
|
||||
if (e.classList.contains("used")) e.classList.remove("used")
|
||||
}
|
||||
(() => {
|
||||
const run = () => {
|
||||
document.querySelectorAll(".floating>input").forEach(e => {
|
||||
function checkState() {
|
||||
if (e.value !== "") {
|
||||
if (e.classList.contains("used")) return;
|
||||
e.classList.add("used")
|
||||
} else {
|
||||
if (e.classList.contains("used")) e.classList.remove("used")
|
||||
}
|
||||
}
|
||||
|
||||
e.addEventListener("change", () => checkState())
|
||||
checkState()
|
||||
})
|
||||
}
|
||||
|
||||
e.addEventListener("change", () => checkState())
|
||||
checkState()
|
||||
})
|
||||
run();
|
||||
|
||||
var mutationObserver = new MutationObserver(() => {
|
||||
run()
|
||||
});
|
||||
|
||||
mutationObserver.observe(document.documentElement, {
|
||||
attributes: false,
|
||||
characterData: false,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
window.Mutt
|
||||
window.addEventListener("DOMNodeInserted", () => run())
|
||||
})();
|
2
views/shared/preact.min.js
vendored
Normal file
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;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
float: left;
|
||||
.permission {
|
||||
display: flex;
|
||||
img {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
.text {
|
||||
// width: calc(100% - 60px);
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
display: block;
|
||||
width: calc(100% - 60px);
|
||||
height: 50px;
|
||||
float: right;
|
||||
padding-left: 10px;
|
||||
}
|
||||
// .image {
|
||||
// height: 50px;
|
||||
// width: 50px;
|
||||
// }
|
||||
|
||||
// .text {
|
||||
// // width: calc(100% - 60px);
|
||||
// padding-left: 10px;
|
||||
// }
|
||||
|
||||
.scope_title {
|
||||
margin-top: 0;
|
||||
|
@ -7,7 +7,8 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<hgroup>
|
||||
<div id="content"></div>
|
||||
{{!-- <hgroup>
|
||||
<h1>{{i18n "Login"}}</h1>
|
||||
</hgroup>
|
||||
<form action="JavaScript:void(0)">
|
||||
@ -15,33 +16,41 @@
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
<div id="container">
|
||||
<div class="floating group" id="usernamegroup">
|
||||
<input type="text" id="username" autofocus>
|
||||
<span class="highlight"></span>
|
||||
<span class="bar"></span>
|
||||
<label>{{i18n "Username or Email"}}</label>
|
||||
<div class="error invisible" id="uerrorfield"></div>
|
||||
<div id="usernamegroup">
|
||||
<div class="floating group">
|
||||
<input type="text" id="username" autofocus>
|
||||
<span class="highlight"></span>
|
||||
<span class="bar"></span>
|
||||
<label>{{i18n "Username or Email"}}</label>
|
||||
<div class="error invisible" id="uerrorfield"></div>
|
||||
</div>
|
||||
<button type="button" id="nextbutton" class="mdc-button mdc-button--raised">{{i18n "Next"}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="floating group invisible" id="passwordgroup">
|
||||
<input type="password" id="password">
|
||||
<span class="highlight"></span>
|
||||
<span class="bar"></span>
|
||||
<label>{{i18n "Password"}}</label>
|
||||
<div class="error invisible" id="perrorfield"></div>
|
||||
<div id="passwordgroup">
|
||||
<div class="floating group invisible" id="passwordgroup">
|
||||
<input type="password" id="password">
|
||||
<span class="highlight"></span>
|
||||
<span class="bar"></span>
|
||||
<label>{{i18n "Password"}}</label>
|
||||
<div class="error invisible" id="perrorfield"></div>
|
||||
</div>
|
||||
<button type="button" id="loginbutton" class="mdc-button mdc-button--raised">{{i18n "Login"}}
|
||||
</button>
|
||||
</div>
|
||||
<div id="twofactorgroup">
|
||||
<ul id="tflist">
|
||||
</ul>
|
||||
|
||||
<button type="button" id="nextbutton" class="mdc-button mdc-button--raised">{{i18n "Next"}}
|
||||
</button>
|
||||
<button type="button" id="loginbutton" class="mdc-button mdc-button--raised invisible">{{i18n "Login"}}
|
||||
</button>
|
||||
<div id="tfinput">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<footer>
|
||||
<!-- <a href="http://www.polymer-project.org/" target="_blank">
|
||||
<img src="https://www.polymer-project.org/images/logos/p-logo.svg">
|
||||
</a> -->
|
||||
<p>Powered by {{appname}}</p>
|
||||
</footer>
|
||||
</footer> --}}
|
||||
</body>
|
||||
|
||||
</html>
|
@ -55,9 +55,7 @@ nextbutton.onclick = async () => {
|
||||
})
|
||||
salt = res.salt;
|
||||
usernamegroup.classList.add("invisible")
|
||||
nextbutton.classList.add("invisible")
|
||||
passwordgroup.classList.remove("invisible")
|
||||
loginbutton.classList.remove("invisible")
|
||||
passwordinput.focus()
|
||||
} catch (e) {
|
||||
showError(uerrorfield, e.message)
|
||||
@ -94,22 +92,26 @@ loginbutton.onclick = async () => {
|
||||
return data;
|
||||
})
|
||||
|
||||
setCookie("login", login.token, login.expires)
|
||||
setCookie("special", special.token, special.expires)
|
||||
setCookie("login", login.token, new Date(login.expires).toUTCString());
|
||||
setCookie("special", special.token, new Date(special.expires).toUTCString());
|
||||
let d = new Date()
|
||||
d.setTime(d.getTime() + (365 * 24 * 60 * 60 * 1000));
|
||||
d.setTime(d.getTime() + (30 * 24 * 60 * 60 * 1000)); //Keep the username 30 days
|
||||
setCookie("username", username, d.toUTCString());
|
||||
let url = new URL(window.location.href);
|
||||
let state = url.searchParams.get("state")
|
||||
let red = "/"
|
||||
if (state) {
|
||||
let base64 = url.searchParams.get("base64")
|
||||
if (base64)
|
||||
red = atob(state)
|
||||
else
|
||||
red = state
|
||||
|
||||
if (tfa) twofactor(tfa);
|
||||
else {
|
||||
if (state) {
|
||||
let base64 = url.searchParams.get("base64")
|
||||
if (base64)
|
||||
red = atob(state)
|
||||
else
|
||||
red = state
|
||||
}
|
||||
window.location.href = red;
|
||||
}
|
||||
window.location.href = red;
|
||||
} catch (e) {
|
||||
passwordinput.value = "";
|
||||
showError(perrorfield, e.message);
|
||||
@ -134,4 +136,23 @@ if (username) {
|
||||
var evt = document.createEvent("HTMLEvents");
|
||||
evt.initEvent("change", false, true);
|
||||
usernameinput.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
|
||||
function twofactor(tfa) {
|
||||
let list = tfa
|
||||
.map(entry => {
|
||||
switch (entry) {
|
||||
case 0: // OTC
|
||||
return "Authenticator App";
|
||||
case 1: // BACKUP
|
||||
return "Backup Key";
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(e => e !== undefined)
|
||||
.reduce((p, c) => p + `<li>${c}</li>`, "");
|
||||
|
||||
let tfl = document.getElementById("tflist");
|
||||
tfl.innerHTML = list;
|
||||
}
|
@ -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;
|
||||
}
|
Reference in New Issue
Block a user