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) ## [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 @@ ...@@ -13,7 +13,8 @@
"simplesamlphp/composer-module-installer": "~1.0", "simplesamlphp/composer-module-installer": "~1.0",
"simplesamlphp/simplesamlphp": "^1.17", "simplesamlphp/simplesamlphp": "^1.17",
"cesnet/privacyidea-php-client": "^1.2.0", "cesnet/privacyidea-php-client": "^1.2.0",
"ext-json": "*" "ext-json": "*",
"beheh/flaps": "^0.2.0"
}, },
"config": { "config": {
"platform": { "platform": {
......
...@@ -4,8 +4,65 @@ ...@@ -4,8 +4,65 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "23d8db8716bb10bb63a2b03a2741ac74", "content-hash": "dbbd35b95607b508fec2914553edaba9",
"packages": [ "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", "name": "cesnet/privacyidea-php-client",
"version": "v1.2.0", "version": "v1.2.0",
...@@ -5012,5 +5069,5 @@ ...@@ -5012,5 +5069,5 @@
"platform-overrides": { "platform-overrides": {
"php": "7.4" "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 ...@@ -144,6 +144,15 @@ You need to add the authentication source 'privacyidea' to
'otplen' => 'otpLength' '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. * Override (string) or reformat (callable) messages from privacyIDEA.
* When using callable, HTML is not escaped. * 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 ...@@ -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. * Whether to show logout link on the auth proc filter page.
* Optional, default to true. * 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 ...@@ -141,9 +141,12 @@ class PrivacyideaAuthProc extends ProcessingFilter
) { ) {
// Call /validate/check with a static pass from the configuration // 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 // 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'], 'otp' => $this->authProcConfig['tryFirstAuthPass'],
]); ]
);
if (empty($response->multiChallenge) && $response->value) { if (empty($response->multiChallenge) && $response->value) {
ProcessingChain::resumeProcessing($state); ProcessingChain::resumeProcessing($state);
} elseif (!empty($response->multiChallenge)) { } elseif (!empty($response->multiChallenge)) {
...@@ -155,12 +158,18 @@ class PrivacyideaAuthProc extends ProcessingFilter ...@@ -155,12 +158,18 @@ class PrivacyideaAuthProc extends ProcessingFilter
// This is AuthProcFilter, so step 1 (username+password) is already done. Set the step to 2 // This is AuthProcFilter, so step 1 (username+password) is already done. Set the step to 2
$state['privacyidea:privacyidea:ui']['step'] = 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'); $stateId = State::saveState($state, 'privacyidea:privacyidea');
$url = Module::getModuleURL('privacyidea/FormBuilder.php'); $url = Module::getModuleURL('privacyidea/FormBuilder.php');
HTTP::redirectTrustedURL($url, [ HTTP::redirectTrustedURL(
$url,
[
'stateId' => $stateId, 'stateId' => $stateId,
]); ]
);
} }
/** /**
...@@ -321,21 +330,27 @@ class PrivacyideaAuthProc extends ProcessingFilter ...@@ -321,21 +330,27 @@ class PrivacyideaAuthProc extends ProcessingFilter
if (!empty($matchedAttrs)) { if (!empty($matchedAttrs)) {
$ret = true; $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 . 'list, but excluded by at least one attribute regexp "' . $attrKey .
'" = "' . $matchedAttrs[0] . '".'); '" = "' . $matchedAttrs[0] . '".'
);
break; break;
} }
} }
} else { } else {
Logger::debug('privacyidea:checkEntityID: attribute key ' . Logger::debug(
$attrKey . ' not contained in request'); 'privacyidea:checkEntityID: attribute key ' .
$attrKey . ' not contained in request'
);
} }
} }
} }
} else { } else {
Logger::debug('privacyidea:checkEntityID: Requesting entityID ' . Logger::debug(
$requestEntityID . ' not matched by any regexp.'); 'privacyidea:checkEntityID: Requesting entityID ' .
$requestEntityID . ' not matched by any regexp.'
);
} }
$state[$setPath][$setKey][0] = $ret; $state[$setPath][$setKey][0] = $ret;
......
...@@ -76,9 +76,9 @@ class PrivacyideaAuthSource extends UserPassBase ...@@ -76,9 +76,9 @@ class PrivacyideaAuthSource extends UserPassBase
// SSO check if authentication should be skipped // SSO check if authentication should be skipped
if ( if (
array_key_exists('SSO', $this->authSourceConfig) && array_key_exists('SSO', $this->authSourceConfig)
$this->authSourceConfig['SSO'] === true && && $this->authSourceConfig['SSO'] === true
Utils::checkForValidSSO($state) && Utils::checkForValidSSO($state)
) { ) {
$session = Session::getSessionFromRequest(); $session = Session::getSessionFromRequest();
$attributes = $session->getData('privacyidea:privacyidea', 'attributes'); $attributes = $session->getData('privacyidea:privacyidea', 'attributes');
...@@ -107,12 +107,18 @@ class PrivacyideaAuthSource extends UserPassBase ...@@ -107,12 +107,18 @@ class PrivacyideaAuthSource extends UserPassBase
$state['privacyidea:privacyidea:ui']['loadCounter'] = '1'; $state['privacyidea:privacyidea:ui']['loadCounter'] = '1';
$state['privacyidea:privacyidea:ui']['messageOverride'] = $this->authSourceConfig['messageOverride'] ?? null; $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'); $stateId = State::saveState($state, 'privacyidea:privacyidea');
$url = Module::getModuleURL('privacyidea/FormBuilder.php'); $url = Module::getModuleURL('privacyidea/FormBuilder.php');
HTTP::redirectTrustedURL($url, [ HTTP::redirectTrustedURL(
$url,
[
'stateId' => $stateId, 'stateId' => $stateId,
]); ]
);
} }
/** /**
...@@ -196,9 +202,12 @@ class PrivacyideaAuthSource extends UserPassBase ...@@ -196,9 +202,12 @@ class PrivacyideaAuthSource extends UserPassBase
//Logger::error("NEW STEP: " . $state['privacyidea:privacyidea:ui']['step']); //Logger::error("NEW STEP: " . $state['privacyidea:privacyidea:ui']['step']);
$stateId = State::saveState($state, 'privacyidea:privacyidea'); $stateId = State::saveState($state, 'privacyidea:privacyidea');
$url = Module::getModuleURL('privacyidea/FormBuilder.php'); $url = Module::getModuleURL('privacyidea/FormBuilder.php');
HTTP::redirectTrustedURL($url, [ HTTP::redirectTrustedURL(
$url,
[
'stateId' => $stateId, 'stateId' => $stateId,
]); ]
);
} }
/** /**
......
...@@ -25,9 +25,14 @@ if ($this->data['errorCode'] !== null) { ...@@ -25,9 +25,14 @@ if ($this->data['errorCode'] !== null) {
<strong> <strong>
<?php <?php
echo htmlspecialchars( echo htmlspecialchars(
sprintf('%s%s: %s', $this->t( sprintf(
'{privacyidea:privacyidea:error}' '%s%s: %s',
), $this->data['errorCode'] ? (' ' . $this->data['errorCode']) : '', $this->data['errorMessage']) $this->t(
'{privacyidea:privacyidea:error}'
),
$this->data['errorCode'] ? (' ' . $this->data['errorCode']) : '',
$this->data['errorMessage']
)
); ?> ); ?>
</strong> </strong>
</p> </p>
...@@ -79,7 +84,7 @@ if ($this->data['errorCode'] !== null) { ...@@ -79,7 +84,7 @@ if ($this->data['errorCode'] !== null) {
<?php <?php
} }
// Remember username in authproc // Remember username in authproc
if (!$this->data['authProcFilterScenario']) { if (!$this->data['authProcFilterScenario']) {
if ($this->data['rememberUsernameEnabled'] || $this->data['rememberMeEnabled']) { if ($this->data['rememberUsernameEnabled'] || $this->data['rememberMeEnabled']) {
$rowspan = 1; $rowspan = 1;
......
...@@ -105,9 +105,12 @@ if (empty($_REQUEST['loadCounter'])) { ...@@ -105,9 +105,12 @@ if (empty($_REQUEST['loadCounter'])) {
} }
if ($state['privacyidea:privacyidea']['authenticationMethod'] === 'authprocess') { 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'], 'as' => $state['Source']['auth'],
]) . '&logout'; ]
) . '&logout';
} }
$translator = $tpl->getTranslator(); $translator = $tpl->getTranslator();
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
declare(strict_types=1); declare(strict_types=1);
use BehEh\Flaps\Flaps;
use BehEh\Flaps\Violation\PassiveViolationHandler;
use SimpleSAML\Auth\Source; use SimpleSAML\Auth\Source;
use SimpleSAML\Auth\State; use SimpleSAML\Auth\State;
use SimpleSAML\Logger; use SimpleSAML\Logger;
...@@ -11,6 +13,7 @@ use SimpleSAML\Module\privacyidea\Auth\Source\PrivacyideaAuthSource; ...@@ -11,6 +13,7 @@ use SimpleSAML\Module\privacyidea\Auth\Source\PrivacyideaAuthSource;
use SimpleSAML\Module\privacyidea\Auth\Utils; use SimpleSAML\Module\privacyidea\Auth\Utils;
use SimpleSAML\Session; use SimpleSAML\Session;
use SimpleSAML\Utils\HTTP; use SimpleSAML\Utils\HTTP;
use SimpleSAML\Module\privacyidea\Auth\MongoDBStorage;
$stateId = Session::getSessionFromRequest()->getData('privacyidea:privacyidea', 'stateId'); $stateId = Session::getSessionFromRequest()->getData('privacyidea:privacyidea', 'stateId');
Session::getSessionFromRequest()->deleteData('privacyidea:privacyidea', 'stateId'); Session::getSessionFromRequest()->deleteData('privacyidea:privacyidea', 'stateId');
...@@ -19,6 +22,19 @@ if (empty($stateId)) { ...@@ -19,6 +22,19 @@ if (empty($stateId)) {
throw new \Exception('State information lost!'); throw new \Exception('State information lost!');
} }
$state = State::loadState($stateId, 'privacyidea:privacyidea'); $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 // Find the username
if (array_key_exists('username', $_REQUEST)) { if (array_key_exists('username', $_REQUEST)) {
...@@ -34,6 +50,21 @@ if (array_key_exists('username', $_REQUEST)) { ...@@ -34,6 +50,21 @@ if (array_key_exists('username', $_REQUEST)) {
$username = ''; $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 = [ $formParams = [
'username' => $username, 'username' => $username,
'pass' => array_key_exists('password', $_REQUEST) ? $_REQUEST['password'] : '', 'pass' => array_key_exists('password', $_REQUEST) ? $_REQUEST['password'] : '',
...@@ -65,9 +96,12 @@ if ($state['privacyidea:privacyidea']['authenticationMethod'] === 'authprocess') ...@@ -65,9 +96,12 @@ if ($state['privacyidea:privacyidea']['authenticationMethod'] === 'authprocess')
$stateId = Utils::processPIResponse($stateId, $response); $stateId = Utils::processPIResponse($stateId, $response);
} }
$url = Module::getModuleURL('privacyidea/FormBuilder.php'); $url = Module::getModuleURL('privacyidea/FormBuilder.php');
HTTP::redirectTrustedURL($url, [ HTTP::redirectTrustedURL(
$url,
[
'stateId' => $stateId, 'stateId' => $stateId,
]); ]
);
} catch (Exception $e) { } catch (Exception $e) {
Logger::error($e->getMessage()); Logger::error($e->getMessage());
} }
......
...@@ -367,8 +367,8 @@ var pi_webauthn = navigator.credentials ? window.pi_webauthn || {} : null; ...@@ -367,8 +367,8 @@ var pi_webauthn = navigator.credentials ? window.pi_webauthn || {} : null;
* *
* @returns {Promise<WebAuthnRegisterResponse>} - Information to pass to /token/init. * @returns {Promise<WebAuthnRegisterResponse>} - Information to pass to /token/init.
* *
* @typedef WebAuthnRegisterRequest * @typedef WebAuthnRegisterRequest
* @type {object} * @type {object}
* @property {string} transaction_id - The transaction id from privacyIDEA. * @property {string} transaction_id - The transaction id from privacyIDEA.
* @property {string} message - Unused. * @property {string} message - Unused.
* @property {string} serialNumber - The serial number of the new token being enrolled. * @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; ...@@ -384,8 +384,8 @@ var pi_webauthn = navigator.credentials ? window.pi_webauthn || {} : null;
* @property {sequence<AAGUID>} [authenticatorSelectionList] - A whitelist of authenticators to allow. * @property {sequence<AAGUID>} [authenticatorSelectionList] - A whitelist of authenticators to allow.
* @property {string} [description] - A description for the token being created. * @property {string} [description] - A description for the token being created.
* *
* @typedef WebAuthnRegisterResponse * @typedef WebAuthnRegisterResponse
* @type {object} * @type {object}
* @property {'webauthn'} type - The token type of the token being enrolled. * @property {'webauthn'} type - The token type of the token being enrolled.
* @property {string} transaction_id - The transaction_id that was passed in. * @property {string} transaction_id - The transaction_id that was passed in.
* @property {string} clientdata - The clientDataJSON, encoded in base64. * @property {string} clientdata - The clientDataJSON, encoded in base64.
...@@ -465,16 +465,16 @@ var pi_webauthn = navigator.credentials ? window.pi_webauthn || {} : null; ...@@ -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`. * @returns {Promise<WebAuthnSignResponse>} - Data for /validate/check minus `user`, `pass`, and `transaction_id`.
* *
* @typedef WebAuthnSignRequest * @typedef WebAuthnSignRequest
* @type {object} * @type {object}
* @property {string} challenge - The challenge from privacyIDEA. * @property {string} challenge - The challenge from privacyIDEA.
* @property {{id: string, type: PublicKeyCredentialType, transports: AuthenticatorTransport[]}[]} allowCredentials - Creds to try. * @property {{id: string, type: PublicKeyCredentialType, transports: AuthenticatorTransport[]}[]} allowCredentials - Creds to try.
* @property {string} rpId - The relying party id the credential was created with. * @property {string} rpId - The relying party id the credential was created with.
* @property {UserVerificationRequirement} userVerification - Option to discourage, or require user verification. * @property {UserVerificationRequirement} userVerification - Option to discourage, or require user verification.
* @property {number} [timeout=60000] - Timeout in milliseconds. * @property {number} [timeout=60000] - Timeout in milliseconds.
* *
* @typedef WebAuthnSignResponse * @typedef WebAuthnSignResponse
* @type {object} * @type {object}
* @property {string} credentialid - The id of the credential being used. * @property {string} credentialid - The id of the credential being used.
* @property {string} clientdata - The clientDataJSON, encoded in base64. * @property {string} clientdata - The clientDataJSON, encoded in base64.
* @property {string} signaturedata - The signature, encoded in base64. * @property {string} signaturedata - The signature, encoded in base64.
...@@ -529,4 +529,4 @@ var pi_webauthn = navigator.credentials ? window.pi_webauthn || {} : null; ...@@ -529,4 +529,4 @@ var pi_webauthn = navigator.credentials ? window.pi_webauthn || {} : null;
return Promise.resolve(webAuthnSignResponse); return Promise.resolve(webAuthnSignResponse);
}); });
}; };
}.bind(pi_webauthn)(navigator.credentials)); }).bind(pi_webauthn)(navigator.credentials);
...@@ -11,18 +11,21 @@ ...@@ -11,18 +11,21 @@
/** /**
* Namespace for the U2F api. * Namespace for the U2F api.
*
* @type {Object} * @type {Object}
*/ */
var u2f = u2f || {}; var u2f = u2f || {};
/** /**
* FIDO U2F Javascript API Version * FIDO U2F Javascript API Version
*
* @number * @number
*/ */
var js_api_version; var js_api_version;
/** /**
* The U2F extension id * The U2F extension id
*
* @const {string} * @const {string}
*/ */
// The Chrome packaged app extension ID. // The Chrome packaged app extension ID.
...@@ -36,8 +39,9 @@ u2f.EXTENSION_ID = "kmendfapggjehodndflmmgagdbamhnfd"; ...@@ -36,8 +39,9 @@ u2f.EXTENSION_ID = "kmendfapggjehodndflmmgagdbamhnfd";
/** /**
* Message types for messsages to/from the extension * Message types for messsages to/from the extension
*
* @const * @const
* @enum {string} * @enum {string}
*/ */
u2f.MessageTypes = { u2f.MessageTypes = {
U2F_REGISTER_REQUEST: "u2f_register_request", U2F_REGISTER_REQUEST: "u2f_register_request",
...@@ -50,8 +54,9 @@ u2f.MessageTypes = { ...@@ -50,8 +54,9 @@ u2f.MessageTypes = {
/** /**
* Response status codes * Response status codes
*
* @const * @const
* @enum {number} * @enum {number}
*/ */
u2f.ErrorCodes = { u2f.ErrorCodes = {
OK: 0, OK: 0,
...@@ -64,6 +69,7 @@ u2f.ErrorCodes = { ...@@ -64,6 +69,7 @@ u2f.ErrorCodes = {
/** /**
* A message for registration requests * A message for registration requests
*
* @typedef {{ * @typedef {{
* type: u2f.MessageTypes, * type: u2f.MessageTypes,
* appId: ?string, * appId: ?string,
...@@ -75,6 +81,7 @@ u2f.U2fRequest; ...@@ -75,6 +81,7 @@ u2f.U2fRequest;
/** /**
* A message for registration responses * A message for registration responses
*
* @typedef {{ * @typedef {{
* type: u2f.MessageTypes, * type: u2f.MessageTypes,
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
...@@ -85,6 +92,7 @@ u2f.U2fResponse; ...@@ -85,6 +92,7 @@ u2f.U2fResponse;
/** /**
* An error object for responses * An error object for responses
*
* @typedef {{ * @typedef {{
* errorCode: u2f.ErrorCodes, * errorCode: u2f.ErrorCodes,
* errorMessage: ?string * errorMessage: ?string
...@@ -94,18 +102,21 @@ u2f.Error; ...@@ -94,18 +102,21 @@ u2f.Error;
/** /**
* Data object for a single sign request. * Data object for a single sign request.
*
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
*/ */
u2f.Transport; u2f.Transport;
/** /**
* Data object for a single sign request. * Data object for a single sign request.
*
* @typedef {Array<u2f.Transport>} * @typedef {Array<u2f.Transport>}
*/ */
u2f.Transports; u2f.Transports;
/** /**
* Data object for a single sign request. * Data object for a single sign request.
*
* @typedef {{ * @typedef {{
* version: string, * version: string,
* challenge: string, * challenge: string,
...@@ -117,6 +128,7 @@ u2f.SignRequest; ...@@ -117,6 +128,7 @@ u2f.SignRequest;
/** /**
* Data object for a sign response. * Data object for a sign response.
*
* @typedef {{ * @typedef {{
* keyHandle: string, * keyHandle: string,
* signatureData: string, * signatureData: string,
...@@ -127,6 +139,7 @@ u2f.SignResponse; ...@@ -127,6 +139,7 @@ u2f.SignResponse;
/** /**
* Data object for a registration request. * Data object for a registration request.
*
* @typedef {{ * @typedef {{
* version: string, * version: string,
* challenge: string * challenge: string
...@@ -136,6 +149,7 @@ u2f.RegisterRequest; ...@@ -136,6 +149,7 @@ u2f.RegisterRequest;
/** /**
* Data object for a registration response. * Data object for a registration response.
*
* @typedef {{ * @typedef {{
* version: string, * version: string,
* keyHandle: string, * keyHandle: string,
...@@ -147,6 +161,7 @@ u2f.RegisterResponse; ...@@ -147,6 +161,7 @@ u2f.RegisterResponse;
/** /**
* Data object for a registered key. * Data object for a registered key.
*
* @typedef {{ * @typedef {{
* version: string, * version: string,
* keyHandle: string, * keyHandle: string,
...@@ -158,6 +173,7 @@ u2f.RegisteredKey; ...@@ -158,6 +173,7 @@ u2f.RegisteredKey;
/** /**
* Data object for a get API register response. * Data object for a get API register response.
*
* @typedef {{ * @typedef {{
* js_api_version: number * js_api_version: number
* }} * }}
...@@ -169,6 +185,7 @@ u2f.GetJsApiVersionResponse; ...@@ -169,6 +185,7 @@ u2f.GetJsApiVersionResponse;
/** /**
* Sets up a MessagePort to the U2F extension using the * Sets up a MessagePort to the U2F extension using the
* available mechanisms. * available mechanisms.
*
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
*/ */
u2f.getMessagePort = function (callback) { u2f.getMessagePort = function (callback) {
...@@ -204,6 +221,7 @@ u2f.getMessagePort = function (callback) { ...@@ -204,6 +221,7 @@ u2f.getMessagePort = function (callback) {
/** /**
* Detect chrome running on android based on the browser's useragent. * Detect chrome running on android based on the browser's useragent.
*
* @private * @private
*/ */
u2f.isAndroidChrome_ = function () { u2f.isAndroidChrome_ = function () {
...@@ -215,6 +233,7 @@ u2f.isAndroidChrome_ = function () { ...@@ -215,6 +233,7 @@ u2f.isAndroidChrome_ = function () {
/** /**
* Detect chrome running on iOS based on the browser's platform. * Detect chrome running on iOS based on the browser's platform.
*
* @private * @private
*/ */
u2f.isIosChrome_ = function () { u2f.isIosChrome_ = function () {
...@@ -223,7 +242,8 @@ u2f.isIosChrome_ = function () { ...@@ -223,7 +242,8 @@ u2f.isIosChrome_ = function () {
/** /**
* Connects directly to the extension via chrome.runtime.connect. * Connects directly to the extension via chrome.runtime.connect.
* @param {function(u2f.WrappedChromeRuntimePort_)} callback *
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
* @private * @private
*/ */
u2f.getChromeRuntimePort_ = function (callback) { u2f.getChromeRuntimePort_ = function (callback) {
...@@ -237,7 +257,8 @@ u2f.getChromeRuntimePort_ = function (callback) { ...@@ -237,7 +257,8 @@ u2f.getChromeRuntimePort_ = function (callback) {
/** /**
* Return a 'port' abstraction to the Authenticator app. * Return a 'port' abstraction to the Authenticator app.
* @param {function(u2f.WrappedAuthenticatorPort_)} callback *
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
* @private * @private
*/ */
u2f.getAuthenticatorPort_ = function (callback) { u2f.getAuthenticatorPort_ = function (callback) {
...@@ -248,7 +269,8 @@ u2f.getAuthenticatorPort_ = function (callback) { ...@@ -248,7 +269,8 @@ u2f.getAuthenticatorPort_ = function (callback) {
/** /**
* Return a 'port' abstraction to the iOS client app. * Return a 'port' abstraction to the iOS client app.
* @param {function(u2f.WrappedIosPort_)} callback *
* @param {function(u2f.WrappedIosPort_)} callback
* @private * @private
*/ */
u2f.getIosPort_ = function (callback) { u2f.getIosPort_ = function (callback) {
...@@ -259,7 +281,8 @@ u2f.getIosPort_ = function (callback) { ...@@ -259,7 +281,8 @@ u2f.getIosPort_ = function (callback) {
/** /**
* A wrapper for chrome.runtime.Port that is compatible with MessagePort. * A wrapper for chrome.runtime.Port that is compatible with MessagePort.
* @param {Port} port *
* @param {Port} port
* @constructor * @constructor
* @private * @private
*/ */
...@@ -269,9 +292,10 @@ u2f.WrappedChromeRuntimePort_ = function (port) { ...@@ -269,9 +292,10 @@ u2f.WrappedChromeRuntimePort_ = function (port) {
/** /**
* Format and return a sign request compliant with the JS API version supported by the extension. * 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 {Array<u2f.SignRequest>} signRequests
* @param {number} reqId * @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object} * @return {Object}
*/ */
u2f.formatSignRequest_ = function ( u2f.formatSignRequest_ = function (
...@@ -312,10 +336,11 @@ 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.. * 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 {Array<u2f.SignRequest>} signRequests
* @param {number} timeoutSeconds * @param {Array<u2f.RegisterRequest>} signRequests
* @param {number} reqId * @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object} * @return {Object}
*/ */
u2f.formatRegisterRequest_ = function ( u2f.formatRegisterRequest_ = function (
...@@ -360,6 +385,7 @@ u2f.formatRegisterRequest_ = function ( ...@@ -360,6 +385,7 @@ u2f.formatRegisterRequest_ = function (
/** /**
* Posts a message on the underlying channel. * Posts a message on the underlying channel.
*
* @param {Object} message * @param {Object} message
*/ */
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (message) { u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (message) {
...@@ -369,6 +395,7 @@ 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 * Emulates the HTML 5 addEventListener interface. Works only for the
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
*
* @param {string} eventName * @param {string} eventName
* @param {function({data: Object})} handler * @param {function({data: Object})} handler
*/ */
...@@ -389,6 +416,7 @@ u2f.WrappedChromeRuntimePort_.prototype.addEventListener = function ( ...@@ -389,6 +416,7 @@ u2f.WrappedChromeRuntimePort_.prototype.addEventListener = function (
/** /**
* Wrap the Authenticator app with a MessagePort interface. * Wrap the Authenticator app with a MessagePort interface.
*
* @constructor * @constructor
* @private * @private
*/ */
...@@ -399,6 +427,7 @@ u2f.WrappedAuthenticatorPort_ = function () { ...@@ -399,6 +427,7 @@ u2f.WrappedAuthenticatorPort_ = function () {
/** /**
* Launch the Authenticator intent. * Launch the Authenticator intent.
*
* @param {Object} message * @param {Object} message
*/ */
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) { u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) {
...@@ -412,6 +441,7 @@ u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) { ...@@ -412,6 +441,7 @@ u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) {
/** /**
* Tells what type of port this is. * Tells what type of port this is.
*
* @return {String} port type * @return {String} port type
*/ */
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () { u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () {
...@@ -420,6 +450,7 @@ u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () { ...@@ -420,6 +450,7 @@ u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () {
/** /**
* Emulates the HTML 5 addEventListener interface. * Emulates the HTML 5 addEventListener interface.
*
* @param {string} eventName * @param {string} eventName
* @param {function({data: Object})} handler * @param {function({data: Object})} handler
*/ */
...@@ -444,6 +475,7 @@ u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function ( ...@@ -444,6 +475,7 @@ u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function (
/** /**
* Callback invoked when a response is received from the Authenticator. * Callback invoked when a response is received from the Authenticator.
*
* @param function({data: Object}) callback * @param function({data: Object}) callback
* @param {Object} message message Object * @param {Object} message message Object
*/ */
...@@ -457,7 +489,9 @@ u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = function ( ...@@ -457,7 +489,9 @@ u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = function (
var errorCode = messageObject["errorCode"]; var errorCode = messageObject["errorCode"];
var responseObject = null; var responseObject = null;
if (messageObject.hasOwnProperty("data")) { if (messageObject.hasOwnProperty("data")) {
responseObject = /** @type {Object} */ (JSON.parse(messageObject["data"])); responseObject = /**
* @type {Object}
*/ (JSON.parse(messageObject["data"]));
} }
callback({ data: responseObject }); callback({ data: responseObject });
...@@ -465,6 +499,7 @@ u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = function ( ...@@ -465,6 +499,7 @@ u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = function (
/** /**
* Base URL for intents to Authenticator. * Base URL for intents to Authenticator.
*
* @const * @const
* @private * @private
*/ */
...@@ -473,6 +508,7 @@ u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = ...@@ -473,6 +508,7 @@ u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
/** /**
* Wrap the iOS client app with a MessagePort interface. * Wrap the iOS client app with a MessagePort interface.
*
* @constructor * @constructor
* @private * @private
*/ */
...@@ -480,6 +516,7 @@ u2f.WrappedIosPort_ = function () {}; ...@@ -480,6 +516,7 @@ u2f.WrappedIosPort_ = function () {};
/** /**
* Launch the iOS client app request * Launch the iOS client app request
*
* @param {Object} message * @param {Object} message
*/ */
u2f.WrappedIosPort_.prototype.postMessage = function (message) { u2f.WrappedIosPort_.prototype.postMessage = function (message) {
...@@ -490,6 +527,7 @@ u2f.WrappedIosPort_.prototype.postMessage = function (message) { ...@@ -490,6 +527,7 @@ u2f.WrappedIosPort_.prototype.postMessage = function (message) {
/** /**
* Tells what type of port this is. * Tells what type of port this is.
*
* @return {String} port type * @return {String} port type
*/ */
u2f.WrappedIosPort_.prototype.getPortType = function () { u2f.WrappedIosPort_.prototype.getPortType = function () {
...@@ -498,6 +536,7 @@ u2f.WrappedIosPort_.prototype.getPortType = function () { ...@@ -498,6 +536,7 @@ u2f.WrappedIosPort_.prototype.getPortType = function () {
/** /**
* Emulates the HTML 5 addEventListener interface. * Emulates the HTML 5 addEventListener interface.
*
* @param {string} eventName * @param {string} eventName
* @param {function({data: Object})} handler * @param {function({data: Object})} handler
*/ */
...@@ -510,7 +549,8 @@ u2f.WrappedIosPort_.prototype.addEventListener = function (eventName, handler) { ...@@ -510,7 +549,8 @@ u2f.WrappedIosPort_.prototype.addEventListener = function (eventName, handler) {
/** /**
* Sets up an embedded trampoline iframe, sourced from the extension. * Sets up an embedded trampoline iframe, sourced from the extension.
* @param {function(MessagePort)} callback *
* @param {function(MessagePort)} callback
* @private * @private
*/ */
u2f.getIframePort_ = function (callback) { u2f.getIframePort_ = function (callback) {
...@@ -543,34 +583,39 @@ u2f.getIframePort_ = function (callback) { ...@@ -543,34 +583,39 @@ u2f.getIframePort_ = function (callback) {
/** /**
* Default extension response timeout in seconds. * Default extension response timeout in seconds.
*
* @const * @const
*/ */
u2f.EXTENSION_TIMEOUT_SEC = 30; u2f.EXTENSION_TIMEOUT_SEC = 30;
/** /**
* A singleton instance for a MessagePort to the extension. * A singleton instance for a MessagePort to the extension.
* @type {MessagePort|u2f.WrappedChromeRuntimePort_} *
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
* @private * @private
*/ */
u2f.port_ = null; u2f.port_ = null;
/** /**
* Callbacks waiting for a port * Callbacks waiting for a port
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>} *
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
* @private * @private
*/ */
u2f.waitingForPort_ = []; u2f.waitingForPort_ = [];
/** /**
* A counter for requestIds. * A counter for requestIds.
* @type {number} *
* @type {number}
* @private * @private
*/ */
u2f.reqCounter_ = 0; u2f.reqCounter_ = 0;
/** /**
* A map from requestIds to client callbacks * 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)))>} * |function((u2f.Error|u2f.SignResponse)))>}
* @private * @private
*/ */
...@@ -578,7 +623,8 @@ u2f.callbackMap_ = {}; ...@@ -578,7 +623,8 @@ u2f.callbackMap_ = {};
/** /**
* Creates or retrieves the MessagePort singleton to use. * Creates or retrieves the MessagePort singleton to use.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback *
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
* @private * @private
*/ */
u2f.getPortSingleton_ = function (callback) { u2f.getPortSingleton_ = function (callback) {
...@@ -590,12 +636,15 @@ u2f.getPortSingleton_ = function (callback) { ...@@ -590,12 +636,15 @@ u2f.getPortSingleton_ = function (callback) {
u2f.port_ = port; u2f.port_ = port;
u2f.port_.addEventListener( u2f.port_.addEventListener(
"message", "message",
/** @type {function(Event)} */ (u2f.responseHandler_) /**
* @type {function(Event)}
*/ (u2f.responseHandler_)
); );
// Careful, here be async callbacks. Maybe. // Careful, here be async callbacks. Maybe.
while (u2f.waitingForPort_.length) while (u2f.waitingForPort_.length) {
u2f.waitingForPort_.shift()(u2f.port_); u2f.waitingForPort_.shift()(u2f.port_);
}
}); });
} }
u2f.waitingForPort_.push(callback); u2f.waitingForPort_.push(callback);
...@@ -604,7 +653,8 @@ u2f.getPortSingleton_ = function (callback) { ...@@ -604,7 +653,8 @@ u2f.getPortSingleton_ = function (callback) {
/** /**
* Handles response messages from the extension. * Handles response messages from the extension.
* @param {MessageEvent.<u2f.Response>} message *
* @param {MessageEvent.<u2f.Response>} message
* @private * @private
*/ */
u2f.responseHandler_ = function (message) { u2f.responseHandler_ = function (message) {
...@@ -624,6 +674,7 @@ 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 * 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 * message to the extension to find out the supported API version and then it sends
* the sign request. * the sign request.
*
* @param {string=} appId * @param {string=} appId
* @param {string=} challenge * @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys * @param {Array<u2f.RegisteredKey>} registeredKeys
...@@ -667,6 +718,7 @@ u2f.sign = function ( ...@@ -667,6 +718,7 @@ u2f.sign = function (
/** /**
* Dispatches an array of sign requests to available U2F tokens. * Dispatches an array of sign requests to available U2F tokens.
*
* @param {string=} appId * @param {string=} appId
* @param {string=} challenge * @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys * @param {Array<u2f.RegisteredKey>} registeredKeys
...@@ -704,6 +756,7 @@ u2f.sendSignRequest = function ( ...@@ -704,6 +756,7 @@ u2f.sendSignRequest = function (
* If the JS API version supported by the extension is unknown, it first sends a * 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 * message to the extension to find out the supported API version and then it sends
* the register request. * the register request.
*
* @param {string=} appId * @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests * @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys * @param {Array<u2f.RegisteredKey>} registeredKeys
...@@ -748,6 +801,7 @@ u2f.register = function ( ...@@ -748,6 +801,7 @@ u2f.register = function (
/** /**
* Dispatches register requests to available U2F tokens. An array of sign * Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens. * requests identifies already registered tokens.
*
* @param {string=} appId * @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests * @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys * @param {Array<u2f.RegisteredKey>} registeredKeys
...@@ -784,6 +838,7 @@ u2f.sendRegisterRequest = function ( ...@@ -784,6 +838,7 @@ u2f.sendRegisterRequest = function (
* JS API version. * JS API version.
* If the user is on a mobile phone and is thus using Google Authenticator instead * 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. * of the Chrome extension, don't send the request and simply return 0.
*
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
* @param {number=} opt_timeoutSeconds * @param {number=} opt_timeoutSeconds
*/ */
......