Added U2F Support for YubiKey

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

View File

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

2395
views/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

18
views/tsconfig.json Normal file
View File

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

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

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