Merge pull request #300 from clems4ever/fix-u2f
Fix U2F authentication by upgrading U2F libraries.pull/302/head
commit
1d6dd9323b
|
@ -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) {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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({
|
||||
registration: {
|
||||
publicKey: "PUBKEY",
|
||||
keyHandle: "KeyHandle"
|
||||
}
|
||||
}));
|
||||
mocks.userDataStore.retrieveU2FRegistrationStub
|
||||
.returns(BluebirdPromise.resolve({
|
||||
registration: {
|
||||
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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue