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 U2f = require("u2f");
import U2fApi = require("u2f-api-polyfill"); import U2fApi from "u2f-api";
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import { SignMessage } from "../../../../shared/SignMessage"; import { SignMessage } from "../../../../shared/SignMessage";
import Endpoints = require("../../../../shared/api"); 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) function startU2fAuthentication($: JQueryStatic, notifier: INotifier)
: BluebirdPromise<string> { : BluebirdPromise<string> {
return GetPromised($, Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {}, return GetPromised($, Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {},
undefined, "json") undefined, "json")
.then(function (signResponse: SignMessage) { .then(function (signRequest: U2f.Request) {
notifier.info(UserMessages.PLEASE_TOUCH_TOKEN); notifier.info(UserMessages.PLEASE_TOUCH_TOKEN);
return U2fApi.sign(signRequest, 60);
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);
}) })
.then(function (signResponse: U2fApi.SignResponse) { .then(function (signResponse: U2fApi.SignResponse) {
return finishU2fAuthentication(signResponse, $); return finishU2fAuthentication(signResponse, $);
}); });
} }
export function validate($: JQueryStatic, notifier: INotifier) { export function validate($: JQueryStatic, notifier: INotifier) {
return startU2fAuthentication($, notifier) return startU2fAuthentication($, notifier)
.catch(function (err: Error) { .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 TOTPValidator = require("./TOTPValidator");
import U2FValidator = require("./U2FValidator"); import U2FValidator = require("./U2FValidator");
import ClientConstants = require("./constants"); import ClientConstants = require("./constants");

View File

@ -1,8 +1,7 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import U2f = require("u2f"); import U2f = require("u2f");
import U2fApi = require("u2f-api-polyfill"); import * as U2fApi from "u2f-api";
import jslogger = require("js-logger");
import { Notifier } from "../Notifier"; import { Notifier } from "../Notifier";
import GetPromised from "../GetPromised"; import GetPromised from "../GetPromised";
import Endpoints = require("../../../../shared/api"); 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> { function requestRegistration(): BluebirdPromise<string> {
return GetPromised($, Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, {}, return GetPromised($, Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, {},
undefined, "json") undefined, "json")
.then((registrationRequest: U2f.Request) => { .then((registrationRequest: U2f.Request) => {
return register(registrationRequest.appId, registrationRequest, 60); return U2fApi.register(registrationRequest, [], 60);
}) })
.then((res) => checkRegistration(res)); .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": { "fill-range": {
"version": "2.2.3", "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, "dev": true,
"requires": { "requires": {
"is-number": "^2.1.0", "is-number": "^2.1.0",
@ -10851,10 +10852,10 @@
"resolved": "https://registry.npmjs.org/u2f/-/u2f-0.1.3.tgz", "resolved": "https://registry.npmjs.org/u2f/-/u2f-0.1.3.tgz",
"integrity": "sha512-/IaxeBqjo5o3D7plPkxdApbCpgGoI2bmTomS1kq5OjVflaE9UBJ0WfqoXqZryZKfFYBjQC7Tn1hA57WtRgh/Sg==" "integrity": "sha512-/IaxeBqjo5o3D7plPkxdApbCpgGoI2bmTomS1kq5OjVflaE9UBJ0WfqoXqZryZKfFYBjQC7Tn1hA57WtRgh/Sg=="
}, },
"u2f-api-polyfill": { "u2f-api": {
"version": "0.4.4", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/u2f-api-polyfill/-/u2f-api-polyfill-0.4.4.tgz", "resolved": "https://registry.npmjs.org/u2f-api/-/u2f-api-1.0.7.tgz",
"integrity": "sha512-qg3LBBHzN46zNE+ySChra8i9PecrWk83DmEkxxMJ9wAy8wV3FGJi6gtV32L+pCIP+kTaxhIvxQe2k76OMuHe9Q==" "integrity": "sha512-aey5tGk4hfw+EMKveDt0IQbM/5VCcACUBRpKU4iU42J6aD9xnmUH6aXFTVWkgfXsNKotbaNW0Tq4L1FKArI4bQ=="
}, },
"uc.micro": { "uc.micro": {
"version": "1.0.5", "version": "1.0.5",

View File

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

View File

@ -4,9 +4,7 @@ import BluebirdPromise = require("bluebird");
import assert = require("assert"); import assert = require("assert");
import U2FSignRequestGet = require("./get"); import U2FSignRequestGet = require("./get");
import ExpressMock = require("../../../../stubs/express.spec"); import ExpressMock = require("../../../../stubs/express.spec");
import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; import { Request } from "u2f";
import U2FMock = require("../../../../stubs/u2f.spec");
import U2f = require("u2f");
import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec";
import { ServerVariables } from "../../../../ServerVariables"; import { ServerVariables } from "../../../../ServerVariables";
@ -40,10 +38,6 @@ describe("routes/secondfactor/u2f/sign_request/get", function () {
mocks = s.mocks; mocks = s.mocks;
vars = s.variables; vars = s.variables;
const options = {
inMemoryOnly: true
};
res = ExpressMock.ResponseMock(); res = ExpressMock.ResponseMock();
res.send = sinon.spy(); res.send = sinon.spy();
res.json = 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 () { it("should send back the sign request and save it in the session", function () {
const expectedRequest: U2f.RegistrationResult = { const expectedRequest: Request = {
keyHandle: "keyHandle", version: "U2F_V2",
publicKey: "publicKey", appId: 'app',
certificate: "Certificate", challenge: 'challenge!'
successful: true
}; };
mocks.u2f.requestStub.returns(expectedRequest); mocks.u2f.requestStub.returns(expectedRequest);
mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ mocks.userDataStore.retrieveU2FRegistrationStub
.returns(BluebirdPromise.resolve({
registration: { registration: {
publicKey: "PUBKEY",
keyHandle: "KeyHandle" keyHandle: "KeyHandle"
} }
})); }));
return U2FSignRequestGet.default(vars)(req as any, res as any) return U2FSignRequestGet.default(vars)(req as any, res as any)
.then(function () { .then(() => {
assert.deepEqual(expectedRequest, req.session.auth.sign_request); 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 u2f_common = require("../../../secondfactor/u2f/U2FCommon");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import express = require("express"); import express = require("express");
import { UserDataStore } from "../../../../storage/UserDataStore";
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
import { Winston } from "../../../../../../types/Dependencies";
import exceptions = require("../../../../Exceptions"); import exceptions = require("../../../../Exceptions");
import { SignMessage } from "../../../../../../../shared/SignMessage";
import ErrorReplies = require("../../../../ErrorReplies"); import ErrorReplies = require("../../../../ErrorReplies");
import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler";
import UserMessages = require("../../../../../../../shared/UserMessages"); import UserMessages = require("../../../../../../../shared/UserMessages");
@ -27,7 +22,7 @@ export default function (vars: ServerVariables) {
.then(function () { .then(function () {
return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId);
}) })
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<SignMessage> { .then(function (doc: U2FRegistrationDocument): BluebirdPromise<void> {
if (!doc) if (!doc)
return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration found")); 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)); vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle));
const request = vars.u2f.request(appId, doc.registration.keyHandle); const request = vars.u2f.request(appId, doc.registration.keyHandle);
const authenticationMessage: SignMessage = { res.json(request);
request: request, authSession.sign_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);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}) })
.catch(ErrorReplies.replyWithError200(req, res, vars.logger, .catch(ErrorReplies.replyWithError200(req, res, vars.logger,