Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • perun/perun-proxyidp/simplesamlphp-module-privacyidea
1 result
Show changes
Commits on Source (3)
# [5.5.0](https://gitlab.ics.muni.cz/perun-proxy-aai/simplesamlphp/simplesamlphp-module-privacyidea/compare/v5.4.2...v5.5.0) (2023-06-27)
### Features
* rate limiting ([c458740](https://gitlab.ics.muni.cz/perun-proxy-aai/simplesamlphp/simplesamlphp-module-privacyidea/commit/c458740d9cd18004b018326a433d7633ebff70a1))
## [5.4.2](https://gitlab.ics.muni.cz/perun-proxy-aai/simplesamlphp/simplesamlphp-module-privacyidea/compare/v5.4.1...v5.4.2) (2022-10-19)
......
......@@ -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": {
......
......@@ -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"
}
......@@ -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.
......
<?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')]]);
}
}
......@@ -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;
......
......@@ -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,
]);
]
);
}
/**
......
......@@ -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;
......
......@@ -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();
......
......@@ -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());
}
......
......@@ -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);
......@@ -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
*/
......