diff --git a/client/src/index.ts b/client/src/index.ts index 802004a8..6c22d17c 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -22,11 +22,11 @@ jslogger.setLevel(jslogger.INFO); if (window.location.pathname == Endpoints.FIRST_FACTOR_GET) FirstFactor(window, jQuery, FirstFactorValidator, jslogger); else if (window.location.pathname == Endpoints.SECOND_FACTOR_GET) - SecondFactor(window, jQuery, (global as any).u2f); + SecondFactor(window, jQuery); else if (window.location.pathname == Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET) TOTPRegister(window, jQuery); else if (window.location.pathname == Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET) - U2fRegister(window, jQuery, (global as any).u2f); + U2fRegister(window, jQuery); else if (window.location.pathname == Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET) ResetPasswordForm(window, jQuery); else if (window.location.pathname == Endpoints.RESET_PASSWORD_REQUEST_GET) diff --git a/client/src/lib/GetPromised.ts b/client/src/lib/GetPromised.ts index e4f435b5..77913965 100644 --- a/client/src/lib/GetPromised.ts +++ b/client/src/lib/GetPromised.ts @@ -2,12 +2,12 @@ import BluebirdPromise = require("bluebird"); export default function ($: JQueryStatic, url: string, data: Object, fn: any, dataType: string): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { + return new BluebirdPromise((resolve, reject) => { $.get(url, {}, undefined, dataType) - .done(function (data: any) { + .done((data: any) => { resolve(data); }) - .fail(function (xhr: JQueryXHR, textStatus: string) { + .fail((xhr: JQueryXHR, textStatus: string) => { reject(textStatus); }); }); diff --git a/client/src/lib/secondfactor/U2FValidator.ts b/client/src/lib/secondfactor/U2FValidator.ts index e24122ed..fdaf639b 100644 --- a/client/src/lib/secondfactor/U2FValidator.ts +++ b/client/src/lib/secondfactor/U2FValidator.ts @@ -1,5 +1,5 @@ import U2f = require("u2f"); -import U2fApi = require("../../../types/u2f-api"); +import U2fApi = require("u2f-api-polyfill"); import BluebirdPromise = require("bluebird"); import { SignMessage } from "../../../../shared/SignMessage"; import Endpoints = require("../../../../shared/api"); @@ -31,15 +31,15 @@ function finishU2fAuthentication(responseData: U2fApi.SignResponse, }); } -function u2fApiSign(u2fApi: U2fApi.U2fApi, appId: string, challenge: string, +function u2fApiSign(appId: string, challenge: string, registeredKey: U2fApi.RegisteredKey, timeout: number) : BluebirdPromise { return new BluebirdPromise(function (resolve, reject) { - u2fApi.sign(appId, challenge, [registeredKey], - function (signResponse: U2fApi.SignResponse | U2fApi.Error) { - if ("errorCode" in signResponse) { - reject(new Error((signResponse as U2fApi.Error).errorMessage)); + (window).u2f.sign(appId, challenge, [registeredKey], + function (signResponse: U2fApi.SignResponse | U2fApi.U2FError) { + if ((signResponse).errorCode != 0) { + reject(new Error((signResponse as U2fApi.U2FError).errorMessage)); return; } resolve(signResponse as U2fApi.SignResponse); @@ -47,8 +47,8 @@ function u2fApiSign(u2fApi: U2fApi.U2fApi, appId: string, challenge: string, }); } -function startU2fAuthentication($: JQueryStatic, notifier: INotifier, - u2fApi: U2fApi.U2fApi): BluebirdPromise { +function startU2fAuthentication($: JQueryStatic, notifier: INotifier) + : BluebirdPromise { return GetPromised($, Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {}, undefined, "json") @@ -58,10 +58,11 @@ function startU2fAuthentication($: JQueryStatic, notifier: INotifier, const registeredKey: U2fApi.RegisteredKey = { keyHandle: signResponse.keyHandle, version: "U2F_V2", - appId: signResponse.request.appId + appId: signResponse.request.appId, + transports: [] }; - return u2fApiSign(u2fApi, signResponse.request.appId, + return u2fApiSign(signResponse.request.appId, signResponse.request.challenge, registeredKey, 60); }) .then(function (signResponse: U2fApi.SignResponse) { @@ -70,9 +71,8 @@ function startU2fAuthentication($: JQueryStatic, notifier: INotifier, } -export function validate($: JQueryStatic, notifier: INotifier, - u2fApi: U2fApi.U2fApi) { - return startU2fAuthentication($, notifier, u2fApi) +export function validate($: JQueryStatic, notifier: INotifier) { + return startU2fAuthentication($, notifier) .catch(function (err: Error) { notifier.error(UserMessages.U2F_TRANSACTION_FINISH_FAILED); return BluebirdPromise.reject(err); diff --git a/client/src/lib/secondfactor/index.ts b/client/src/lib/secondfactor/index.ts index bc5eb78f..a345807a 100644 --- a/client/src/lib/secondfactor/index.ts +++ b/client/src/lib/secondfactor/index.ts @@ -1,5 +1,5 @@ import jslogger = require("js-logger"); -import U2fApi = require("../../../types/u2f-api"); +import U2fApi = require("u2f-api-polyfill"); import TOTPValidator = require("./TOTPValidator"); import U2FValidator = require("./U2FValidator"); import ClientConstants = require("./constants"); @@ -10,7 +10,7 @@ import ServerConstants = require("../../../../shared/constants"); import UserMessages = require("../../../../shared/UserMessages"); import SharedConstants = require("../../../../shared/constants"); -export default function (window: Window, $: JQueryStatic, u2fApi: U2fApi.U2fApi) { +export default function (window: Window, $: JQueryStatic) { const notifierTotp = new Notifier(".notification-totp", $); const notifierU2f = new Notifier(".notification-u2f", $); @@ -49,7 +49,7 @@ export default function (window: Window, $: JQueryStatic, u2fApi: U2fApi.U2fApi) $(window.document).ready(function () { $(ClientConstants.TOTP_FORM_SELECTOR).on("submit", onTOTPFormSubmitted); - U2FValidator.validate($, notifierU2f, u2fApi) + U2FValidator.validate($, notifierU2f) .then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure); }); } \ No newline at end of file diff --git a/client/src/lib/u2f-register/u2f-register.ts b/client/src/lib/u2f-register/u2f-register.ts index f996a808..cc15258e 100644 --- a/client/src/lib/u2f-register/u2f-register.ts +++ b/client/src/lib/u2f-register/u2f-register.ts @@ -1,7 +1,7 @@ import BluebirdPromise = require("bluebird"); import U2f = require("u2f"); -import U2fApi = require("u2f-api"); +import U2fApi = require("u2f-api-polyfill"); import jslogger = require("js-logger"); import { Notifier } from "../Notifier"; import GetPromised from "../GetPromised"; @@ -10,38 +10,35 @@ import UserMessages = require("../../../../shared/UserMessages"); import { RedirectionMessage } from "../../../../shared/RedirectionMessage"; import { ErrorMessage } from "../../../../shared/ErrorMessage"; -export default function (window: Window, $: JQueryStatic, u2fApi: U2fApi.U2fApi) { +export default function (window: Window, $: JQueryStatic) { const notifier = new Notifier(".notification", $); function checkRegistration(regResponse: U2fApi.RegisterResponse): BluebirdPromise { - const registrationData: U2f.RegistrationData = regResponse; - return new BluebirdPromise(function (resolve, reject) { - $.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, registrationData, undefined, "json") - .done(function (body: RedirectionMessage | ErrorMessage) { + $.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, regResponse, undefined, "json") + .done((body: RedirectionMessage | ErrorMessage) => { if (body && "error" in body) { reject(new Error((body as ErrorMessage).error)); return; } resolve((body as RedirectionMessage).redirect); }) - .fail(function (xhr, status) { - reject(); + .fail((xhr, status) => { + reject(new Error("Failed to register device.")); }); }); } - function u2fApiRegister(u2fApi: U2fApi.U2fApi, appId: string, - registerRequest: U2fApi.RegisterRequest, timeout: number) { - - return new BluebirdPromise(function (resolve, reject) { - u2fApi.register(appId, [registerRequest], [], - function (res: U2fApi.RegisterResponse | U2fApi.Error) { - if ("errorCode" in res) { - reject(new Error((res as U2fApi.Error).errorMessage)); + function register(appId: string, registerRequest: U2fApi.RegisterRequest, + timeout: number): BluebirdPromise { + return new BluebirdPromise((resolve, reject) => { + (window as any).u2f.register(appId, [registerRequest], [], + (res: U2fApi.RegisterResponse | U2fApi.U2FError) => { + if ((res).errorCode != 0) { + reject(new Error((res).errorMessage)); return; } - resolve(res); + resolve(res); }, timeout); }); } @@ -49,14 +46,10 @@ export default function (window: Window, $: JQueryStatic, u2fApi: U2fApi.U2fApi) function requestRegistration(): BluebirdPromise { return GetPromised($, Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, {}, undefined, "json") - .then(function (registrationRequest: U2f.Request) { - const registerRequest: U2fApi.RegisterRequest = registrationRequest; - const appId = registrationRequest.appId; - return u2fApiRegister(u2fApi, appId, registerRequest, 60); + .then((registrationRequest: U2f.Request) => { + return register(registrationRequest.appId, registrationRequest, 60); }) - .then(function (res: U2fApi.RegisterResponse) { - return checkRegistration(res); - }); + .then((res) => checkRegistration(res)); } function onRegisterFailure(err: Error) { @@ -65,10 +58,10 @@ export default function (window: Window, $: JQueryStatic, u2fApi: U2fApi.U2fApi) $(document).ready(function () { requestRegistration() - .then(function (redirectionUrl: string) { + .then((redirectionUrl: string) => { document.location.href = redirectionUrl; }) - .error(function (err) { + .catch((err) => { onRegisterFailure(err); }); }); diff --git a/client/src/thirdparties/u2f-api.js b/client/src/thirdparties/u2f-api.js deleted file mode 100644 index 8c7801e3..00000000 --- a/client/src/thirdparties/u2f-api.js +++ /dev/null @@ -1,749 +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 - -/** - * @fileoverview The U2F api. - */ -'use strict'; - - -/** - * Namespace for the U2F api. - * @type {Object} - */ -var u2f = 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}} - */ -u2f.Transport; - - -/** - * Data object for a single sign request. - * @typedef {Array} - */ -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} 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} signRequests - * @param {Array} 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} - * @private - */ -u2f.waitingForPort_ = []; - -/** - * A counter for requestIds. - * @type {number} - * @private - */ -u2f.reqCounter_ = 0; - -/** - * A map from requestIds to client callbacks - * @type {Object.} - * @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.} 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} 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} 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} registerRequests - * @param {Array} 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} registerRequests - * @param {Array} 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); - }); -}; - diff --git a/client/types/u2f-api-polyfill.d.ts b/client/types/u2f-api-polyfill.d.ts new file mode 100644 index 00000000..67ddf4a7 --- /dev/null +++ b/client/types/u2f-api-polyfill.d.ts @@ -0,0 +1,167 @@ +// Base 64 using `-` and `_`, without trailing `=`. +// See: +// - https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html#key-words +// - https://tools.ietf.org/html/rfc4648#section-5 +type WebSafeBase64 = string; + +// Blob with fields +type BinaryEncoded = string; + +type URL = String; +type WebOrigin = URL; +type TrustedFacetsURL = URL; + +// U2F Types + +type Challenge = WebSafeBase64; +type Typ = "navigator.id.getAssertion" | "navigator.id.finishEnrollment" + +// TODO: Which fields are optional? +type ClientData = { + typ: Typ, + challenge: Challenge, + origin: WebOrigin + cid_pubkey: "unused" +} + +type RegistrationData = { + keyHandle: string; + publicKey: string; +} + +type SignatureData = { + // TODO +} + +// TODO +type KeyHandle = string; + + +// Polyfill-specific types for `u2f-api-polyfill.d.ts` that are not defined in +// `u2f-api-polyfill` itself. + +type AppID = TrustedFacetsURL; // A URL +type EncodedClientData = WebSafeBase64; // TODO +type EncodedRegistrationData = BinaryEncoded; +type EncodedSignatureData = BinaryEncoded; // TODO +type ErrorMessage = string; +type EncodedKeyHandle = WebSafeBase64; +type PolyfillVersion = "U2F_V2"; // TODO: are other values supported? +type RequestID = number; +type Seconds = number; + +// Types from `u2f-api-polyfill`. + +export const EXTENSION_ID: string; + +export enum MessageType { + 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" +} + +export enum ErrorCode { + OK = 0, + OTHER_ERROR = 1, + BAD_REQUEST = 2, + CONFIGURATION_UNSUPPORTED = 3, + DEVICE_INELIGIBLE = 4, + TIMEOUT = 5 +} + +type U2FError = { + errorCode: ErrorCode, + errorMessage?: ErrorMessage +} + +// TODO: What are the values? +export enum Transport { + BLUETOOTH_RADIO, + BLUETOOTH_LOW_ENERGY, + USB, + NFC +} + +type SignResponse = { + keyHandle: EncodedKeyHandle, + signatureData: EncodedSignatureData, + clientData: EncodedClientData +} + +type RegisterRequest = { + version: PolyfillVersion, + challenge: Challenge +} + +type RegisterResponse = { + version: PolyfillVersion, + challenge: Challenge, + EncodedregistrationData: EncodedRegistrationData, + clientData: EncodedClientData +} + +type RegisteredKey = { + version: PolyfillVersion, + keyHandle: EncodedKeyHandle, + transports: Transport[], + appId?: AppID +} + +type GetJsApiVersionResponse = { + js_api_version: number +} + +// TODO: WrappedChromeRuntimePort_? +export function getMessagePort( + callback: (m: MessagePort) => void +): void; + +// TODO: function formatSignRequest_ is not marked as private? +// TODO: function formatRegisterRequest_ is not marked as private? + +// Default extension response timeout in seconds. +export const EXTENSION_TIMEOUT_SEC: Seconds; + +// 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. +export function sign( + appId: AppID | undefined, + challenge: Challenge | undefined, + registeredKeys: RegisteredKey[], + callback: (response: (U2FError | SignResponse)) => void, + timeout?: Seconds +): void; + +// Dispatches an array of sign requests to available U2F tokens. +export const sendSignRequest: typeof sign; + +// 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. +export function register( + appId: AppID | undefined, + registerRequests: RegisterRequest[], + registeredKeys: RegisteredKey[], + callback: (response: (U2FError | RegisterResponse)) => void, + timeout?: Seconds +): void; + +// Dispatches register requests to available U2F tokens. An array of sign +// requests identifies already registered tokens. +export const sendRegisterRequest: typeof register; + +// 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. +export function getApiVersion( + callback: (response: (U2FError | GetJsApiVersionResponse)) => void, + timeout?: Seconds +): void; \ No newline at end of file diff --git a/client/types/u2f-api.d.ts b/client/types/u2f-api.d.ts deleted file mode 100644 index 7675ca23..00000000 --- a/client/types/u2f-api.d.ts +++ /dev/null @@ -1,73 +0,0 @@ -type MessageTypes = "u2f_register_request" | "u2f_sign_request" | - "u2f_register_response" | "u2f_sign_response"; - -export interface Request { - type: MessageTypes; - signRequests: SignRequest[]; - registerRequests?: RegisterRequest[]; - timeoutSeconds?: number; - requestId?: number; -} - -type ResponseData = Error | RegisterResponse | SignResponse; - - -export interface Response { - type: MessageTypes; - responseData: ResponseData; - requestId?: number; -} - -export enum ErrorCodes { - "OK" = 0, - "OTHER_ERROR" = 1, - "BAD_REQUEST" = 2, - "CONFIGURATION_UNSUPPORTED" = 3, - "DEVICE_INELIGIBLE" = 4, - "TIMEOUT" = 5 -} - -export interface Error { - errorCode: ErrorCodes; - errorMessage?: string; -} - -export interface RegisterResponse { - registrationData: string; - clientData: string; -} - -export interface RegisterRequest { - version: string; - challenge: string; -} - -export interface SignResponse { - keyHandle: string; - signatureData: string; - clientData: string; -} - -export interface SignRequest { - version: string; - challenge: string; - keyHandle: string; - appId: string; -} - -export interface RegisteredKey { - version: string, - keyHandle: string, - transports?: any, - appId?: string -} - -export interface U2fApi { - sign(appId: string, challenge: string, registeredKeys: RegisteredKey[], - cb: (res: SignResponse | Error) => void, timeout: number): void; - - register(appId: string, registerRequests: RegisterRequest[], - registeredKeys: RegisteredKey[], - cb: (res: RegisterResponse | Error) => void, - timeout: number): void; -} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f3647975..1fad8eff 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,6 +1,6 @@ version: '2' services: - authelia: + authelia-dev: build: context: . dockerfile: Dockerfile.dev @@ -18,7 +18,9 @@ services: depends_on: - redis networks: - - example-network + example-network: + aliases: + - authelia command: - "./node_modules/.bin/ts-node" - "-P" diff --git a/package-lock.json b/package-lock.json index d04eb24f..f3f5af0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10893,6 +10893,11 @@ "resolved": "https://registry.npmjs.org/u2f/-/u2f-0.1.3.tgz", "integrity": "sha512-/IaxeBqjo5o3D7plPkxdApbCpgGoI2bmTomS1kq5OjVflaE9UBJ0WfqoXqZryZKfFYBjQC7Tn1hA57WtRgh/Sg==" }, + "u2f-api-polyfill": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/u2f-api-polyfill/-/u2f-api-polyfill-0.4.4.tgz", + "integrity": "sha512-qg3LBBHzN46zNE+ySChra8i9PecrWk83DmEkxxMJ9wAy8wV3FGJi6gtV32L+pCIP+kTaxhIvxQe2k76OMuHe9Q==" + }, "uc.micro": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz", diff --git a/package.json b/package.json index 387bc0bf..bea0a274 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "redis": "^2.8.0", "speakeasy": "^2.0.0", "u2f": "^0.1.2", + "u2f-api-polyfill": "^0.4.4", "winston": "^2.3.1", "yamljs": "^0.3.0" }, diff --git a/server/src/views/secondfactor.pug b/server/src/views/secondfactor.pug index 87b57818..74aae408 100644 --- a/server/src/views/secondfactor.pug +++ b/server/src/views/secondfactor.pug @@ -27,5 +27,4 @@ block content a(href=u2f_identity_start_endpoint, class="link register-u2f", data-toggle="tooltip", title="A security key is required to register.") Security key | | a(href=totp_identity_start_endpoint, class="link register-totp") Google Authenticator - span(class="clearfix") - script(src="/js/u2f-api.js", type="text/javascript") + span(class="clearfix") \ No newline at end of file diff --git a/server/src/views/u2f-register.pug b/server/src/views/u2f-register.pug index d52eba6c..5e24bc70 100644 --- a/server/src/views/u2f-register.pug +++ b/server/src/views/u2f-register.pug @@ -8,5 +8,4 @@ block form-header block content p Touch the token to register your security key. - img(src="/img/pendrive.png" alt="pendrive") - script(src="/js/u2f-api.js", type="text/javascript") \ No newline at end of file + img(src="/img/pendrive.png" alt="pendrive") \ No newline at end of file