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
  */