Merge pull request #300 from clems4ever/fix-u2f

Fix U2F authentication by upgrading U2F libraries.
pull/302/head
Clément Michaud 2018-11-06 16:55:13 +01:00 committed by GitHub
commit 1d6dd9323b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 28 additions and 258 deletions

View File

@ -1,5 +1,5 @@
import U2f = require("u2f");
import U2fApi = require("u2f-api-polyfill");
import U2fApi from "u2f-api";
import BluebirdPromise = require("bluebird");
import { SignMessage } from "../../../../shared/SignMessage";
import Endpoints = require("../../../../shared/api");
@ -31,46 +31,20 @@ function finishU2fAuthentication(responseData: U2fApi.SignResponse,
});
}
function u2fApiSign(appId: string, challenge: string,
registeredKey: U2fApi.RegisteredKey, timeout: number)
: BluebirdPromise<U2fApi.SignResponse> {
return new BluebirdPromise<U2fApi.SignResponse>(function (resolve, reject) {
(<any>window).u2f.sign(appId, challenge, [registeredKey],
function (signResponse: U2fApi.SignResponse | U2fApi.U2FError) {
if ((<U2fApi.U2FError>signResponse).errorCode != 0) {
reject(new Error((signResponse as U2fApi.U2FError).errorMessage));
return;
}
resolve(signResponse as U2fApi.SignResponse);
}, timeout);
});
}
function startU2fAuthentication($: JQueryStatic, notifier: INotifier)
: BluebirdPromise<string> {
return GetPromised($, Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {},
undefined, "json")
.then(function (signResponse: SignMessage) {
.then(function (signRequest: U2f.Request) {
notifier.info(UserMessages.PLEASE_TOUCH_TOKEN);
const registeredKey: U2fApi.RegisteredKey = {
keyHandle: signResponse.keyHandle,
version: "U2F_V2",
appId: signResponse.request.appId,
transports: []
};
return u2fApiSign(signResponse.request.appId,
signResponse.request.challenge, registeredKey, 60);
return U2fApi.sign(signRequest, 60);
})
.then(function (signResponse: U2fApi.SignResponse) {
return finishU2fAuthentication(signResponse, $);
});
}
export function validate($: JQueryStatic, notifier: INotifier) {
return startU2fAuthentication($, notifier)
.catch(function (err: Error) {

View File

@ -1,5 +1,3 @@
import jslogger = require("js-logger");
import U2fApi = require("u2f-api-polyfill");
import TOTPValidator = require("./TOTPValidator");
import U2FValidator = require("./U2FValidator");
import ClientConstants = require("./constants");

View File

@ -1,8 +1,7 @@
import BluebirdPromise = require("bluebird");
import U2f = require("u2f");
import U2fApi = require("u2f-api-polyfill");
import jslogger = require("js-logger");
import * as U2fApi from "u2f-api";
import { Notifier } from "../Notifier";
import GetPromised from "../GetPromised";
import Endpoints = require("../../../../shared/api");
@ -29,25 +28,11 @@ export default function (window: Window, $: JQueryStatic) {
});
}
function register(appId: string, registerRequest: U2fApi.RegisterRequest,
timeout: number): BluebirdPromise<U2fApi.RegisterResponse> {
return new BluebirdPromise((resolve, reject) => {
(window as any).u2f.register(appId, [registerRequest], [],
(res: U2fApi.RegisterResponse | U2fApi.U2FError) => {
if ((<U2fApi.U2FError>res).errorCode != 0) {
reject(new Error((<U2fApi.U2FError>res).errorMessage));
return;
}
resolve(<U2fApi.RegisterResponse>res);
}, timeout);
});
}
function requestRegistration(): BluebirdPromise<string> {
return GetPromised($, Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, {},
undefined, "json")
.then((registrationRequest: U2f.Request) => {
return register(registrationRequest.appId, registrationRequest, 60);
return U2fApi.register(registrationRequest, [], 60);
})
.then((res) => checkRegistration(res));
}

View File

@ -1,167 +0,0 @@
// 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<T> = string;
// Blob with fields
type BinaryEncoded<T> = string;
type URL = String;
type WebOrigin = URL;
type TrustedFacetsURL = URL;
// U2F Types
type Challenge = WebSafeBase64<string>;
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<ClientData>; // TODO
type EncodedRegistrationData = BinaryEncoded<RegistrationData>;
type EncodedSignatureData = BinaryEncoded<SignatureData>; // TODO
type ErrorMessage = string;
type EncodedKeyHandle = WebSafeBase64<KeyHandle>;
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;

11
package-lock.json generated
View File

@ -6276,7 +6276,8 @@
},
"fill-range": {
"version": "2.2.3",
"resolved": "",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz",
"integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=",
"dev": true,
"requires": {
"is-number": "^2.1.0",
@ -10851,10 +10852,10 @@
"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=="
"u2f-api": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/u2f-api/-/u2f-api-1.0.7.tgz",
"integrity": "sha512-aey5tGk4hfw+EMKveDt0IQbM/5VCcACUBRpKU4iU42J6aD9xnmUH6aXFTVWkgfXsNKotbaNW0Tq4L1FKArI4bQ=="
},
"uc.micro": {
"version": "1.0.5",

View File

@ -46,7 +46,7 @@
"redis": "^2.8.0",
"speakeasy": "^2.0.0",
"u2f": "^0.1.2",
"u2f-api-polyfill": "^0.4.4",
"u2f-api": "^1.0.7",
"winston": "^2.3.1",
"yamljs": "^0.3.0"
},

View File

@ -4,9 +4,7 @@ import BluebirdPromise = require("bluebird");
import assert = require("assert");
import U2FSignRequestGet = require("./get");
import ExpressMock = require("../../../../stubs/express.spec");
import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec";
import U2FMock = require("../../../../stubs/u2f.spec");
import U2f = require("u2f");
import { Request } from "u2f";
import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec";
import { ServerVariables } from "../../../../ServerVariables";
@ -40,10 +38,6 @@ describe("routes/secondfactor/u2f/sign_request/get", function () {
mocks = s.mocks;
vars = s.variables;
const options = {
inMemoryOnly: true
};
res = ExpressMock.ResponseMock();
res.send = sinon.spy();
res.json = sinon.spy();
@ -51,24 +45,23 @@ describe("routes/secondfactor/u2f/sign_request/get", function () {
});
it("should send back the sign request and save it in the session", function () {
const expectedRequest: U2f.RegistrationResult = {
keyHandle: "keyHandle",
publicKey: "publicKey",
certificate: "Certificate",
successful: true
const expectedRequest: Request = {
version: "U2F_V2",
appId: 'app',
challenge: 'challenge!'
};
mocks.u2f.requestStub.returns(expectedRequest);
mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({
mocks.userDataStore.retrieveU2FRegistrationStub
.returns(BluebirdPromise.resolve({
registration: {
publicKey: "PUBKEY",
keyHandle: "KeyHandle"
}
}));
return U2FSignRequestGet.default(vars)(req as any, res as any)
.then(function () {
.then(() => {
assert.deepEqual(expectedRequest, req.session.auth.sign_request);
assert.deepEqual(expectedRequest, res.json.getCall(0).args[0].request);
assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]);
});
});
});

View File

@ -1,14 +1,9 @@
import objectPath = require("object-path");
import U2f = require("u2f");
import u2f_common = require("../../../secondfactor/u2f/U2FCommon");
import BluebirdPromise = require("bluebird");
import express = require("express");
import { UserDataStore } from "../../../../storage/UserDataStore";
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
import { Winston } from "../../../../../../types/Dependencies";
import exceptions = require("../../../../Exceptions");
import { SignMessage } from "../../../../../../../shared/SignMessage";
import ErrorReplies = require("../../../../ErrorReplies");
import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler";
import UserMessages = require("../../../../../../../shared/UserMessages");
@ -27,7 +22,7 @@ export default function (vars: ServerVariables) {
.then(function () {
return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId);
})
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<SignMessage> {
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<void> {
if (!doc)
return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration found"));
@ -36,17 +31,8 @@ export default function (vars: ServerVariables) {
vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle));
const request = vars.u2f.request(appId, doc.registration.keyHandle);
const authenticationMessage: SignMessage = {
request: request,
keyHandle: doc.registration.keyHandle
};
return BluebirdPromise.resolve(authenticationMessage);
})
.then(function (authenticationMessage: SignMessage) {
vars.logger.info(req, "Store authentication request and reply");
vars.logger.debug(req, "AuthenticationRequest = %s", authenticationMessage);
authSession.sign_request = authenticationMessage.request;
res.json(authenticationMessage);
res.json(request);
authSession.sign_request = request;
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError200(req, res, vars.logger,