diff --git a/composer.json b/composer.json index 92731d85cc914c0b20be98e03c6210229f411d6e..09acb458a38cf036f0635c9fabc6fede7b46ec34 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "simplesamlphp/composer-module-installer": "~1.0", "simplesamlphp/simplesamlphp": "^1.17", "cesnet/privacyidea-php-client": "^1.2.0", - "ext-json": "*" + "ext-json": "*", + "beheh/flaps": "^0.2.0" }, "config": { "platform": { diff --git a/composer.lock b/composer.lock index 059e099d815eb38c59aa5d15b9f5779415fa737d..048585baafb3b49983742439ae4fbe29acecffa6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,65 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "23d8db8716bb10bb63a2b03a2741ac74", + "content-hash": "dbbd35b95607b508fec2914553edaba9", "packages": [ + { + "name": "beheh/flaps", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/beheh/flaps.git", + "reference": "327110f6c97a3a3e90bd39c47ebb9e2f02db9d61" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beheh/flaps/zipball/327110f6c97a3a3e90bd39c47ebb9e2f02db9d61", + "reference": "327110f6c97a3a3e90bd39c47ebb9e2f02db9d61", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "doctrine/cache": "~1.4", + "phpunit/phpunit": "~4.0", + "predis/predis": "~1.0" + }, + "suggest": { + "doctrine/cache": "Enables a wide variety of caching systems as storage", + "predis/predis": "Enable Redis as storage system" + }, + "type": "library", + "autoload": { + "psr-4": { + "BehEh\\Flaps\\": "src/Flaps" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Benedict Etzel", + "email": "developer@beheh.de", + "homepage": "https://beheh.de", + "role": "Developer" + } + ], + "description": "Modular library for rate limiting requests in applications", + "homepage": "https://github.com/beheh/flaps", + "keywords": [ + "http", + "rate limit", + "throttle" + ], + "support": { + "issues": "https://github.com/beheh/flaps/issues", + "source": "https://github.com/beheh/flaps/tree/master" + }, + "time": "2017-06-02T21:56:31+00:00" + }, { "name": "cesnet/privacyidea-php-client", "version": "v1.2.0", @@ -5012,5 +5069,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/docs/privacyidea.md b/docs/privacyidea.md index fd394dd32380b2a167a081852d36651a65a461ac..19e251f88b1205009a27acff1776abab335b33d4 100644 --- a/docs/privacyidea.md +++ b/docs/privacyidea.md @@ -144,6 +144,15 @@ You need to add the authentication source 'privacyidea' to 'otplen' => 'otpLength' ], + /* + * Setup of MongoDB for Rate Limiting + */ + 'rate_limiting' => [ + 'connection_string' => 'mongodb://user:password@localhost:27017', + 'database_name' => 'mydatabase', + 'collection_prefix' => 'myflaps', + ], + /* * Override (string) or reformat (callable) messages from privacyIDEA. * When using callable, HTML is not escaped. @@ -352,6 +361,15 @@ If you want to use privacyIDEA as an auth process filter, add the configuration ] ], + /** + * Setup of MongoDB for Rate Limiting + */ + 'rate_limiting' => [ + 'connection_string' => 'mongodb://user:password@localhost:27017', + 'database_name' => 'mydatabase', + 'collection_prefix' => 'myflaps', + ], + /** * Whether to show logout link on the auth proc filter page. * Optional, default to true. diff --git a/lib/Auth/MongoDBStorage.php b/lib/Auth/MongoDBStorage.php new file mode 100644 index 0000000000000000000000000000000000000000..5d8f1d1e85257b406ed9ed30bda6542964e73baa --- /dev/null +++ b/lib/Auth/MongoDBStorage.php @@ -0,0 +1,97 @@ +<?php + +namespace SimpleSAML\Module\privacyidea\Auth; + +use Exception; +use MongoDB\Client; +use MongoDB\Collection; +use BehEh\Flaps\StorageInterface; + +class MongoDBStorage implements StorageInterface +{ + private $collection; + + public function __construct($config) + { + $connectionString = $config['connection_string']; + $databaseName = $config['database_name']; + $collectionName = $config['collection_name']; + $client = new Client( + $connectionString, + [], + [ + 'typeMap' => [ + 'array' => 'array', + 'document' => 'array', + 'root' => 'array', + ], + ] + ); + $this->collection = $client->selectCollection($databaseName, $collectionName); + } + + public function setValue($key, $value) + { + $this->collection->updateOne( + ['_id' => $key], + ['$set' => ['value' => $value]], + ['upsert' => true] + ); + } + + public function incrementValue($key) + { + $result = $this->collection->findOneAndUpdate( + ['_id' => $key], + ['$inc' => ['value' => 1]], + ['upsert' => true, 'returnDocument' => Collection::RETURN_DOCUMENT_AFTER] + ); + + return $result ? $result['value'] : 0; + } + + public function getValue($key) + { + $result = $this->collection->findOne(['_id' => $key]); + + $result = $this->collection->findOne( + ['$or' => ['$and' => ['_id' => $key, 'expireAt' => + ['$gt' => gmdate('Y-m-d H:i:s')]]], ['$and' => ['_id' => $key, 'expire' => null]]] + ); + } + + public function setTimestamp($key, $timestamp) + { + $this->collection->updateOne( + ['_id' => $key], + ['$set' => ['timestamp' => $timestamp]], + ['upsert' => true] + ); + } + + public function getTimestamp($key) + { + $result = $this->collection->findOne(['_id' => $key]); + + return $result ? $result['timestamp'] : 0; + } + + public function expire($key) + { + $this->collection->deleteOne(['_id' => $key]); + $this->collection->deleteMany(['expireAt' => ['$lt' => gmdate('Y-m-d H:i:s')]]); + } + + /** + * @throws Exception + */ + public function expireIn($key, $seconds) + { + $this->collection->updateOne( + ['_id' => $key], + ['$set' => ['expireAt' => new DateTimeImmutable('+ ' . $seconds . ' seconds')]], + ['upsert' => true] + ); + $this->collection->deleteMany(['expireAt' => ['$lt' => gmdate('Y-m-d H:i:s')]]); + } +} diff --git a/lib/Auth/Process/PrivacyideaAuthProc.php b/lib/Auth/Process/PrivacyideaAuthProc.php index 53de1bfd3cc6447757cf7cd9d79d518d7b2fbee4..c35d35332620061fb37058231b00b9716fcd014c 100644 --- a/lib/Auth/Process/PrivacyideaAuthProc.php +++ b/lib/Auth/Process/PrivacyideaAuthProc.php @@ -141,9 +141,12 @@ class PrivacyideaAuthProc extends ProcessingFilter ) { // Call /validate/check with a static pass from the configuration // This could already end the authentication with the "passOnNoToken" policy, or it could trigger challenges - $response = Utils::authenticatePI($state, [ + $response = Utils::authenticatePI( + $state, + [ 'otp' => $this->authProcConfig['tryFirstAuthPass'], - ]); + ] + ); if (empty($response->multiChallenge) && $response->value) { ProcessingChain::resumeProcessing($state); } elseif (!empty($response->multiChallenge)) { @@ -155,12 +158,18 @@ class PrivacyideaAuthProc extends ProcessingFilter // This is AuthProcFilter, so step 1 (username+password) is already done. Set the step to 2 $state['privacyidea:privacyidea:ui']['step'] = 2; + if (!empty($this->authProcConfig['rate_limiting'])) { + $state['privacyidea:privacyidea']['rate_limiting'] = $this->authProcConfig['rate_limiting']; + } $stateId = State::saveState($state, 'privacyidea:privacyidea'); $url = Module::getModuleURL('privacyidea/FormBuilder.php'); - HTTP::redirectTrustedURL($url, [ + HTTP::redirectTrustedURL( + $url, + [ 'stateId' => $stateId, - ]); + ] + ); } /** @@ -321,21 +330,27 @@ class PrivacyideaAuthProc extends ProcessingFilter if (!empty($matchedAttrs)) { $ret = true; - Logger::debug('privacyidea:checkEntityID: Requesting entityID in ' . + Logger::debug( + 'privacyidea:checkEntityID: Requesting entityID in ' . 'list, but excluded by at least one attribute regexp "' . $attrKey . - '" = "' . $matchedAttrs[0] . '".'); + '" = "' . $matchedAttrs[0] . '".' + ); break; } } } else { - Logger::debug('privacyidea:checkEntityID: attribute key ' . - $attrKey . ' not contained in request'); + Logger::debug( + 'privacyidea:checkEntityID: attribute key ' . + $attrKey . ' not contained in request' + ); } } } } else { - Logger::debug('privacyidea:checkEntityID: Requesting entityID ' . - $requestEntityID . ' not matched by any regexp.'); + Logger::debug( + 'privacyidea:checkEntityID: Requesting entityID ' . + $requestEntityID . ' not matched by any regexp.' + ); } $state[$setPath][$setKey][0] = $ret; diff --git a/lib/Auth/Source/PrivacyideaAuthSource.php b/lib/Auth/Source/PrivacyideaAuthSource.php index 3f25e25c21ade6916390724edcdb176625baaff4..765658dd6839124be7d19132b1e35ef5e7350efe 100644 --- a/lib/Auth/Source/PrivacyideaAuthSource.php +++ b/lib/Auth/Source/PrivacyideaAuthSource.php @@ -76,9 +76,9 @@ class PrivacyideaAuthSource extends UserPassBase // SSO check if authentication should be skipped if ( - array_key_exists('SSO', $this->authSourceConfig) && - $this->authSourceConfig['SSO'] === true && - Utils::checkForValidSSO($state) + array_key_exists('SSO', $this->authSourceConfig) + && $this->authSourceConfig['SSO'] === true + && Utils::checkForValidSSO($state) ) { $session = Session::getSessionFromRequest(); $attributes = $session->getData('privacyidea:privacyidea', 'attributes'); @@ -107,12 +107,18 @@ class PrivacyideaAuthSource extends UserPassBase $state['privacyidea:privacyidea:ui']['loadCounter'] = '1'; $state['privacyidea:privacyidea:ui']['messageOverride'] = $this->authSourceConfig['messageOverride'] ?? null; + if (!empty($this->authProcConfig['rate_limiting'])) { + $state['privacyidea:privacyidea']['rate_limiting'] = $this->authSourceConfig['rate_limiting']; + } $stateId = State::saveState($state, 'privacyidea:privacyidea'); $url = Module::getModuleURL('privacyidea/FormBuilder.php'); - HTTP::redirectTrustedURL($url, [ + HTTP::redirectTrustedURL( + $url, + [ 'stateId' => $stateId, - ]); + ] + ); } /** @@ -196,9 +202,12 @@ class PrivacyideaAuthSource extends UserPassBase //Logger::error("NEW STEP: " . $state['privacyidea:privacyidea:ui']['step']); $stateId = State::saveState($state, 'privacyidea:privacyidea'); $url = Module::getModuleURL('privacyidea/FormBuilder.php'); - HTTP::redirectTrustedURL($url, [ + HTTP::redirectTrustedURL( + $url, + [ 'stateId' => $stateId, - ]); + ] + ); } /** diff --git a/templates/LoginForm.php b/templates/LoginForm.php index 9a011a1c43b18cc273cd6036af2e11d2ec5e4466..2a80adc48724ce6d30647c5bfee5b74a9f73fe8b 100644 --- a/templates/LoginForm.php +++ b/templates/LoginForm.php @@ -25,9 +25,14 @@ if ($this->data['errorCode'] !== null) { <strong> <?php echo htmlspecialchars( - sprintf('%s%s: %s', $this->t( - '{privacyidea:privacyidea:error}' - ), $this->data['errorCode'] ? (' ' . $this->data['errorCode']) : '', $this->data['errorMessage']) + sprintf( + '%s%s: %s', + $this->t( + '{privacyidea:privacyidea:error}' + ), + $this->data['errorCode'] ? (' ' . $this->data['errorCode']) : '', + $this->data['errorMessage'] + ) ); ?> </strong> </p> @@ -79,7 +84,7 @@ if ($this->data['errorCode'] !== null) { <?php } - // Remember username in authproc + // Remember username in authproc if (!$this->data['authProcFilterScenario']) { if ($this->data['rememberUsernameEnabled'] || $this->data['rememberMeEnabled']) { $rowspan = 1; diff --git a/www/FormBuilder.php b/www/FormBuilder.php index c32ad2e1499d61e47edd39bcc4996a8c2837f058..4345d639f8963f6c57d8ec8c7be52f334257721d 100644 --- a/www/FormBuilder.php +++ b/www/FormBuilder.php @@ -105,9 +105,12 @@ if (empty($_REQUEST['loadCounter'])) { } if ($state['privacyidea:privacyidea']['authenticationMethod'] === 'authprocess') { - $tpl->data['LogoutURL'] = Module::getModuleURL('core/authenticate.php', [ + $tpl->data['LogoutURL'] = Module::getModuleURL( + 'core/authenticate.php', + [ 'as' => $state['Source']['auth'], - ]) . '&logout'; + ] + ) . '&logout'; } $translator = $tpl->getTranslator(); diff --git a/www/FormReceiver.php b/www/FormReceiver.php index 4b82a7b84ea050507c5a6308798a46cbba35389c..6e624fe34fdfb3cb828b0c869ff9302dcb40ebbb 100644 --- a/www/FormReceiver.php +++ b/www/FormReceiver.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use BehEh\Flaps\Flaps; +use BehEh\Flaps\Violation\PassiveViolationHandler; use SimpleSAML\Auth\Source; use SimpleSAML\Auth\State; use SimpleSAML\Logger; @@ -11,6 +13,7 @@ use SimpleSAML\Module\privacyidea\Auth\Source\PrivacyideaAuthSource; use SimpleSAML\Module\privacyidea\Auth\Utils; use SimpleSAML\Session; use SimpleSAML\Utils\HTTP; +use SimpleSAML\Module\privacyidea\Auth\MongoDBStorage; $stateId = Session::getSessionFromRequest()->getData('privacyidea:privacyidea', 'stateId'); Session::getSessionFromRequest()->deleteData('privacyidea:privacyidea', 'stateId'); @@ -19,6 +22,19 @@ if (empty($stateId)) { throw new \Exception('State information lost!'); } $state = State::loadState($stateId, 'privacyidea:privacyidea'); +if (!empty($state['privacyidea:privacyidea']['rate_limiting'])) { + $config = $state['privacyidea:privacyidea']['rate_limiting']; + $usernameStorage = new MongoDBStorage( + array_merge($config, ['collection_name' => $config['collection_prefix'] . 'username']) + ); + $ipStorage = new MongoDBStorage(array_merge($config, ['collection_name' => $config['collection_prefix'] . 'ip'])); + $usernameFlaps = new Flaps($usernameStorage); + $ipFlaps = new Flaps($ipStorage); + $usernameFlaps->setViolationHandler(new PassiveViolationHandler()); + $ipFlaps->setViolationHandler(new PassiveViolationHandler()); + $ip = $_SERVER['REMOTE_ADDR']; +} + // Find the username if (array_key_exists('username', $_REQUEST)) { @@ -34,6 +50,21 @@ if (array_key_exists('username', $_REQUEST)) { $username = ''; } +if (!empty($state['privacyidea:privacyidea']['rate_limiting'])) { + // Limit the requests based on UID and IP + if (!$usernameFlaps->login->limit($username)) { + Logger::warning('Rate limit exceeded for username ' . $username); + $e = new \SimpleSAML\Error\Error('BADREQUEST', null, 429); + throw new \SimpleSAML\Error\Exception('Rate limit exceeded', 429, $e); + } + if (!$ipFlaps->login->limit($ip)) { + Logger::warning('Rate limit exceeded for IP address ' . $ip); + $e = new \SimpleSAML\Error\Error('BADREQUEST', null, 429); + throw new \SimpleSAML\Error\Exception('Rate limit exceeded', 429, $e); + } +} + + $formParams = [ 'username' => $username, 'pass' => array_key_exists('password', $_REQUEST) ? $_REQUEST['password'] : '', @@ -65,9 +96,12 @@ if ($state['privacyidea:privacyidea']['authenticationMethod'] === 'authprocess') $stateId = Utils::processPIResponse($stateId, $response); } $url = Module::getModuleURL('privacyidea/FormBuilder.php'); - HTTP::redirectTrustedURL($url, [ + HTTP::redirectTrustedURL( + $url, + [ 'stateId' => $stateId, - ]); + ] + ); } catch (Exception $e) { Logger::error($e->getMessage()); } diff --git a/www/js/pi-webauthn.js b/www/js/pi-webauthn.js index b70fd0f8c1d7d414984ce04e109aa69b3f47158b..5337ef68f591630c890d48614c25e2a5107afe28 100644 --- a/www/js/pi-webauthn.js +++ b/www/js/pi-webauthn.js @@ -367,8 +367,8 @@ var pi_webauthn = navigator.credentials ? window.pi_webauthn || {} : null; * * @returns {Promise<WebAuthnRegisterResponse>} - Information to pass to /token/init. * - * @typedef WebAuthnRegisterRequest - * @type {object} + * @typedef WebAuthnRegisterRequest + * @type {object} * @property {string} transaction_id - The transaction id from privacyIDEA. * @property {string} message - Unused. * @property {string} serialNumber - The serial number of the new token being enrolled. @@ -384,8 +384,8 @@ var pi_webauthn = navigator.credentials ? window.pi_webauthn || {} : null; * @property {sequence<AAGUID>} [authenticatorSelectionList] - A whitelist of authenticators to allow. * @property {string} [description] - A description for the token being created. * - * @typedef WebAuthnRegisterResponse - * @type {object} + * @typedef WebAuthnRegisterResponse + * @type {object} * @property {'webauthn'} type - The token type of the token being enrolled. * @property {string} transaction_id - The transaction_id that was passed in. * @property {string} clientdata - The clientDataJSON, encoded in base64. @@ -465,16 +465,16 @@ var pi_webauthn = navigator.credentials ? window.pi_webauthn || {} : null; * * @returns {Promise<WebAuthnSignResponse>} - Data for /validate/check minus `user`, `pass`, and `transaction_id`. * - * @typedef WebAuthnSignRequest - * @type {object} + * @typedef WebAuthnSignRequest + * @type {object} * @property {string} challenge - The challenge from privacyIDEA. * @property {{id: string, type: PublicKeyCredentialType, transports: AuthenticatorTransport[]}[]} allowCredentials - Creds to try. * @property {string} rpId - The relying party id the credential was created with. * @property {UserVerificationRequirement} userVerification - Option to discourage, or require user verification. * @property {number} [timeout=60000] - Timeout in milliseconds. * - * @typedef WebAuthnSignResponse - * @type {object} + * @typedef WebAuthnSignResponse + * @type {object} * @property {string} credentialid - The id of the credential being used. * @property {string} clientdata - The clientDataJSON, encoded in base64. * @property {string} signaturedata - The signature, encoded in base64. @@ -529,4 +529,4 @@ var pi_webauthn = navigator.credentials ? window.pi_webauthn || {} : null; return Promise.resolve(webAuthnSignResponse); }); }; -}.bind(pi_webauthn)(navigator.credentials)); +}).bind(pi_webauthn)(navigator.credentials); diff --git a/www/js/u2f-api.js b/www/js/u2f-api.js index 1626ff04bb4f2bb356ad360114733a81ba74af2b..8ad8ca55081215d13d16464f1db18bc6b437d22f 100644 --- a/www/js/u2f-api.js +++ b/www/js/u2f-api.js @@ -11,18 +11,21 @@ /** * 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. @@ -36,8 +39,9 @@ u2f.EXTENSION_ID = "kmendfapggjehodndflmmgagdbamhnfd"; /** * Message types for messsages to/from the extension + * * @const - * @enum {string} + * @enum {string} */ u2f.MessageTypes = { U2F_REGISTER_REQUEST: "u2f_register_request", @@ -50,8 +54,9 @@ u2f.MessageTypes = { /** * Response status codes + * * @const - * @enum {number} + * @enum {number} */ u2f.ErrorCodes = { OK: 0, @@ -64,6 +69,7 @@ u2f.ErrorCodes = { /** * A message for registration requests + * * @typedef {{ * type: u2f.MessageTypes, * appId: ?string, @@ -75,6 +81,7 @@ u2f.U2fRequest; /** * A message for registration responses + * * @typedef {{ * type: u2f.MessageTypes, * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), @@ -85,6 +92,7 @@ u2f.U2fResponse; /** * An error object for responses + * * @typedef {{ * errorCode: u2f.ErrorCodes, * errorMessage: ?string @@ -94,18 +102,21 @@ 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.Transport>} */ u2f.Transports; /** * Data object for a single sign request. + * * @typedef {{ * version: string, * challenge: string, @@ -117,6 +128,7 @@ u2f.SignRequest; /** * Data object for a sign response. + * * @typedef {{ * keyHandle: string, * signatureData: string, @@ -127,6 +139,7 @@ u2f.SignResponse; /** * Data object for a registration request. + * * @typedef {{ * version: string, * challenge: string @@ -136,6 +149,7 @@ u2f.RegisterRequest; /** * Data object for a registration response. + * * @typedef {{ * version: string, * keyHandle: string, @@ -147,6 +161,7 @@ u2f.RegisterResponse; /** * Data object for a registered key. + * * @typedef {{ * version: string, * keyHandle: string, @@ -158,6 +173,7 @@ u2f.RegisteredKey; /** * Data object for a get API register response. + * * @typedef {{ * js_api_version: number * }} @@ -169,6 +185,7 @@ u2f.GetJsApiVersionResponse; /** * Sets up a MessagePort to the U2F extension using the * available mechanisms. + * * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback */ u2f.getMessagePort = function (callback) { @@ -204,6 +221,7 @@ u2f.getMessagePort = function (callback) { /** * Detect chrome running on android based on the browser's useragent. + * * @private */ u2f.isAndroidChrome_ = function () { @@ -215,6 +233,7 @@ u2f.isAndroidChrome_ = function () { /** * Detect chrome running on iOS based on the browser's platform. + * * @private */ u2f.isIosChrome_ = function () { @@ -223,7 +242,8 @@ u2f.isIosChrome_ = function () { /** * Connects directly to the extension via chrome.runtime.connect. - * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * + * @param {function(u2f.WrappedChromeRuntimePort_)} callback * @private */ u2f.getChromeRuntimePort_ = function (callback) { @@ -237,7 +257,8 @@ u2f.getChromeRuntimePort_ = function (callback) { /** * Return a 'port' abstraction to the Authenticator app. - * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * + * @param {function(u2f.WrappedAuthenticatorPort_)} callback * @private */ u2f.getAuthenticatorPort_ = function (callback) { @@ -248,7 +269,8 @@ u2f.getAuthenticatorPort_ = function (callback) { /** * Return a 'port' abstraction to the iOS client app. - * @param {function(u2f.WrappedIosPort_)} callback + * + * @param {function(u2f.WrappedIosPort_)} callback * @private */ u2f.getIosPort_ = function (callback) { @@ -259,7 +281,8 @@ u2f.getIosPort_ = function (callback) { /** * A wrapper for chrome.runtime.Port that is compatible with MessagePort. - * @param {Port} port + * + * @param {Port} port * @constructor * @private */ @@ -269,9 +292,10 @@ u2f.WrappedChromeRuntimePort_ = function (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 + * + * @param {Array<u2f.SignRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId * @return {Object} */ u2f.formatSignRequest_ = function ( @@ -312,10 +336,11 @@ u2f.formatSignRequest_ = function ( /** * 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 + * + * @param {Array<u2f.SignRequest>} signRequests + * @param {Array<u2f.RegisterRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId * @return {Object} */ u2f.formatRegisterRequest_ = function ( @@ -360,6 +385,7 @@ u2f.formatRegisterRequest_ = function ( /** * Posts a message on the underlying channel. + * * @param {Object} message */ u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (message) { @@ -369,6 +395,7 @@ u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (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 */ @@ -389,6 +416,7 @@ u2f.WrappedChromeRuntimePort_.prototype.addEventListener = function ( /** * Wrap the Authenticator app with a MessagePort interface. + * * @constructor * @private */ @@ -399,6 +427,7 @@ u2f.WrappedAuthenticatorPort_ = function () { /** * Launch the Authenticator intent. + * * @param {Object} message */ u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) { @@ -412,6 +441,7 @@ u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) { /** * Tells what type of port this is. + * * @return {String} port type */ u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () { @@ -420,6 +450,7 @@ u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () { /** * Emulates the HTML 5 addEventListener interface. + * * @param {string} eventName * @param {function({data: Object})} handler */ @@ -444,6 +475,7 @@ u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function ( /** * Callback invoked when a response is received from the Authenticator. + * * @param function({data: Object}) callback * @param {Object} message message Object */ @@ -457,7 +489,9 @@ u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = function ( var errorCode = messageObject["errorCode"]; var responseObject = null; if (messageObject.hasOwnProperty("data")) { - responseObject = /** @type {Object} */ (JSON.parse(messageObject["data"])); + responseObject = /** + * @type {Object} + */ (JSON.parse(messageObject["data"])); } callback({ data: responseObject }); @@ -465,6 +499,7 @@ u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = function ( /** * Base URL for intents to Authenticator. + * * @const * @private */ @@ -473,6 +508,7 @@ u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = /** * Wrap the iOS client app with a MessagePort interface. + * * @constructor * @private */ @@ -480,6 +516,7 @@ u2f.WrappedIosPort_ = function () {}; /** * Launch the iOS client app request + * * @param {Object} message */ u2f.WrappedIosPort_.prototype.postMessage = function (message) { @@ -490,6 +527,7 @@ u2f.WrappedIosPort_.prototype.postMessage = function (message) { /** * Tells what type of port this is. + * * @return {String} port type */ u2f.WrappedIosPort_.prototype.getPortType = function () { @@ -498,6 +536,7 @@ u2f.WrappedIosPort_.prototype.getPortType = function () { /** * Emulates the HTML 5 addEventListener interface. + * * @param {string} eventName * @param {function({data: Object})} handler */ @@ -510,7 +549,8 @@ u2f.WrappedIosPort_.prototype.addEventListener = function (eventName, handler) { /** * Sets up an embedded trampoline iframe, sourced from the extension. - * @param {function(MessagePort)} callback + * + * @param {function(MessagePort)} callback * @private */ u2f.getIframePort_ = function (callback) { @@ -543,34 +583,39 @@ u2f.getIframePort_ = function (callback) { /** * 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_} + * + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} * @private */ u2f.port_ = null; /** * Callbacks waiting for a port - * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>} + * + * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>} * @private */ u2f.waitingForPort_ = []; /** * A counter for requestIds. - * @type {number} + * + * @type {number} * @private */ u2f.reqCounter_ = 0; /** * A map from requestIds to client callbacks - * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse)) + * + * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse)) * |function((u2f.Error|u2f.SignResponse)))>} * @private */ @@ -578,7 +623,8 @@ u2f.callbackMap_ = {}; /** * Creates or retrieves the MessagePort singleton to use. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback * @private */ u2f.getPortSingleton_ = function (callback) { @@ -590,12 +636,15 @@ u2f.getPortSingleton_ = function (callback) { u2f.port_ = port; u2f.port_.addEventListener( "message", - /** @type {function(Event)} */ (u2f.responseHandler_) + /** + * @type {function(Event)} + */ (u2f.responseHandler_) ); // Careful, here be async callbacks. Maybe. - while (u2f.waitingForPort_.length) + while (u2f.waitingForPort_.length) { u2f.waitingForPort_.shift()(u2f.port_); + } }); } u2f.waitingForPort_.push(callback); @@ -604,7 +653,8 @@ u2f.getPortSingleton_ = function (callback) { /** * Handles response messages from the extension. - * @param {MessageEvent.<u2f.Response>} message + * + * @param {MessageEvent.<u2f.Response>} message * @private */ u2f.responseHandler_ = function (message) { @@ -624,6 +674,7 @@ u2f.responseHandler_ = function (message) { * 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 @@ -667,6 +718,7 @@ u2f.sign = function ( /** * Dispatches an array of sign requests to available U2F tokens. + * * @param {string=} appId * @param {string=} challenge * @param {Array<u2f.RegisteredKey>} registeredKeys @@ -704,6 +756,7 @@ u2f.sendSignRequest = function ( * 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 @@ -748,6 +801,7 @@ u2f.register = function ( /** * 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 @@ -784,6 +838,7 @@ u2f.sendRegisterRequest = function ( * 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 */