Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix/358 webauthn #366

Merged
merged 6 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lam/HISTORY
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
December 2024 9.0
- Fixed bugs:
-> Windows: show more than 1000 LDAP entries when paged results is activated in server profile
-> WebAuthn: support DNs larger than 64 bytes (358)


24.09.2024 8.9
Expand Down
141 changes: 110 additions & 31 deletions lam/lib/webauthn.inc
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,11 @@ class WebauthnManager {
* Returns if the given DN is registered for webauthn.
*
* @param string $dn DN
* @return boolean is registered
* @return bool is registered
*/
public function isRegistered($dn) {
public function isRegistered($dn): bool {
$database = $this->getDatabase();
$userEntity = $this->getUserEntity($dn);
$results = $database->findAllForUserEntity($userEntity);
$results = $database->findAllForUserDn($dn);
return !empty($results);
}

Expand All @@ -109,7 +108,7 @@ class WebauthnManager {
$userEntity = $this->getUserEntity($dn);
$challenge = $this->createChallenge();
$credentialParameters = $this->getCredentialParameters();
$excludedKeys = $this->getExcludedKeys($userEntity, $extraExcludedKeys);
$excludedKeys = $this->getExcludedKeys($dn, $extraExcludedKeys);
$timeout = $this->getTimeout();
$authenticatorSelectionCriteria = AuthenticatorSelectionCriteria::create()->setUserVerification(AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED);
$registrationObject = new PublicKeyCredentialCreationOptions(
Expand Down Expand Up @@ -184,13 +183,23 @@ class WebauthnManager {
*/
private function getUserEntity($dn) {
return new PublicKeyCredentialUserEntity(
$dn,
$dn,
self::getUserIdFromDn($dn),
self::getUserIdFromDn($dn),
extractRDNValue($dn),
null
);
}

/**
* Generates the user ID as hash of the user DN.
*
* @param string $dn user DN
* @return string user ID
*/
public static function getUserIdFromDn(string $dn) {
return hash('sha256', $dn);
}

/**
* Returns the part that identifies the server and application.
*
Expand Down Expand Up @@ -228,14 +237,14 @@ class WebauthnManager {
/**
* Returns a list of all credential ids that are already registered.
*
* @param PublicKeyCredentialUserEntity $user user data
* @param string $userDn user DN
* @param array $extraExcludedKeys credentialIds that should be added to excluded keys
* @return PublicKeyCredentialDescriptor[] credential ids
*/
private function getExcludedKeys($user, $extraExcludedKeys = []) {
private function getExcludedKeys(string $userDn, $extraExcludedKeys = []) {
$keys = [];
$repository = $this->getDatabase();
$credentialSources = $repository->findAllForUserEntity($user);
$credentialSources = $repository->findAllForUserDn($userDn);
foreach ($credentialSources as $credentialSource) {
$keys[] = new PublicKeyCredentialDescriptor(PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, $credentialSource->getPublicKeyCredentialId());
}
Expand Down Expand Up @@ -357,8 +366,7 @@ class WebauthnManager {
$timeout = $this->getTimeout();
$challenge = $this->createChallenge();
$database = $this->getDatabase();
$userEntity = $this->getUserEntity($userDN);
$publicKeyCredentialSources = $database->findAllForUserEntity($userEntity);
$publicKeyCredentialSources = $database->findAllForUserDn($userDN);
$userVerification = PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
$extensions = new AuthenticationExtensionsClientInputs();
$relyingParty = $this->createRpEntry($isSelfService);
Expand Down Expand Up @@ -406,12 +414,19 @@ class WebauthnManager {
$psrFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
$psr7Request = $psrFactory->createRequest($symfonyRequest);
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::createFromString($_SESSION['webauthn_authentication']);
$credential = $database->findOneByCredentialId($publicKeyCredential->getRawId());
if ($credential === null) {
throw new LAMException(null, 'Unable to find credential');
}
$userId = $credential->getUserHandle();
// old entries use DN as user ID
$userHandle = ($userId === $userDn) ? $userDn : self::getUserIdFromDn($userDn);
$responseValidator->check(
$publicKeyCredential->getRawId(),
$publicKeyCredential->getResponse(),
$publicKeyCredentialRequestOptions,
$psr7Request,
$userDn
$userHandle
);
return true;
}
Expand Down Expand Up @@ -469,11 +484,21 @@ abstract class PublicKeyCredentialSourceRepositoryBase implements PublicKeyCrede
* @return PublicKeyCredentialSource[] credential sources
*/
public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array {
return $this->findAllForUserDn($publicKeyCredentialUserEntity->getName());
}

/**
* Finds all credential entries for the given user.
*
* @param string $userDn user DN
* @return PublicKeyCredentialSource[] credential sources
*/
public function findAllForUserDn(string $userDn): array {
$credentials = [];
try {
$pdo = $this->getPDO();
$statement = $pdo->prepare('select * from ' . $this->getTableName() . ' where userId = :userid');
$statement->execute([':userid' => $publicKeyCredentialUserEntity->getId()]);
$statement = $pdo->prepare('select * from ' . $this->getTableName() . ' where userDN = :userDN');
$statement->execute([':userDN' => $userDn]);
$results = $statement->fetchAll();
foreach ($results as $result) {
$jsonArray = json_decode($result['credentialSource'], true);
Expand All @@ -497,36 +522,43 @@ abstract class PublicKeyCredentialSourceRepositoryBase implements PublicKeyCrede
$userId = $publicKeyCredentialSource->getUserHandle();
$currentTime = time();
$pdo = $this->getPDO();
$statement = $pdo->prepare('select * from ' . $this->getTableName() . ' where userId = :userId and credentialId = :credentialId');
if (isset($_SESSION['selfService_clientDN'])) {
$userDn = lamDecrypt($_SESSION['selfService_clientDN'], 'SelfService');
}
else {
$userDn = $_SESSION['ldap']->getUserName();
}
$statement = $pdo->prepare('select * from ' . $this->getTableName() . ' where userDN = :userDN and credentialId = :credentialId');
$statement->execute([
':userId' => $userId,
':userDN' => $userDn,
':credentialId' => $credentialId
]);
$results = $statement->fetchAll();
if (empty($results)) {
$statement = $pdo->prepare('insert into ' . $this->getTableName() . ' (userId, credentialId, credentialSource, registrationTime, lastUseTime) VALUES (?, ?, ?, ?, ?)');
$statement = $pdo->prepare('insert into ' . $this->getTableName() . ' (userId, credentialId, credentialSource, registrationTime, lastUseTime, userDN) VALUES (?, ?, ?, ?, ?, ?)');
$statement->execute([
$userId,
$credentialId,
$json,
$currentTime,
$currentTime
$currentTime,
$userDn
]);
logNewMessage(LOG_DEBUG, 'Stored new credential for ' . $userId);
logNewMessage(LOG_DEBUG, 'Stored new credential for ' . $userDn);
}
else {
$statement = $pdo->prepare(
'update ' . $this->getTableName() .
' set credentialSource = :credentialSource, lastUseTime = :lastUseTime' .
' WHERE userId = :userId AND credentialId = :credentialId'
' WHERE userDN = :userDN AND credentialId = :credentialId'
);
$statement->execute([
':credentialSource' => $json,
':lastUseTime' => $currentTime,
':userId' => $userId,
':userDN' => $userDn,
':credentialId' => $credentialId
]);
logNewMessage(LOG_DEBUG, 'Stored updated credential for ' . $userId);
logNewMessage(LOG_DEBUG, 'Stored updated credential for ' . $userDn);
}
}

Expand Down Expand Up @@ -557,7 +589,7 @@ abstract class PublicKeyCredentialSourceRepositoryBase implements PublicKeyCrede
*/
public function searchDevices(string $searchTerm) {
$pdo = $this->getPDO();
$statement = $pdo->prepare('select * from ' . $this->getTableName() . ' where userId like :searchTerm order by userId,registrationTime');
$statement = $pdo->prepare('select * from ' . $this->getTableName() . ' where userDN like :searchTerm order by userId,registrationTime');
$statement->execute([
':searchTerm' => $searchTerm
]);
Expand All @@ -566,7 +598,7 @@ abstract class PublicKeyCredentialSourceRepositoryBase implements PublicKeyCrede
foreach ($results as $result) {
$name = !empty($result['name']) ? $result['name'] : '';
$devices[] = [
'dn' => $result['userId'],
'dn' => $result['userDN'],
'credentialId' => $result['credentialId'],
'lastUseTime' => $result['lastUseTime'],
'registrationTime' => $result['registrationTime'],
Expand All @@ -586,9 +618,9 @@ abstract class PublicKeyCredentialSourceRepositoryBase implements PublicKeyCrede
public function deleteDevice(string $dn, string $credentialId) {
logNewMessage(LOG_NOTICE, 'Delete webauthn device ' . $credentialId . ' of ' . $dn);
$pdo = $this->getPDO();
$statement = $pdo->prepare('delete from ' . $this->getTableName() . ' where userId = :userId and credentialId = :credentialId');
$statement = $pdo->prepare('delete from ' . $this->getTableName() . ' where userDN = :userDN and credentialId = :credentialId');
$statement->execute([
':userId' => $dn,
':userDN' => $dn,
':credentialId' => $credentialId
]);
return $statement->rowCount() > 0;
Expand All @@ -604,9 +636,9 @@ abstract class PublicKeyCredentialSourceRepositoryBase implements PublicKeyCrede
*/
public function updateDeviceName(string $dn, string $credentialId, $name) {
$pdo = $this->getPDO();
$statement = $pdo->prepare('update ' . $this->getTableName() . ' set name = :name where userId = :userId and credentialId = :credentialId');
$statement = $pdo->prepare('update ' . $this->getTableName() . ' set name = :name where userDN = :userDN and credentialId = :credentialId');
$statement->execute([
':userId' => $dn,
':userDN' => $dn,
':credentialId' => $credentialId,
':name' => $name
]);
Expand Down Expand Up @@ -636,6 +668,7 @@ abstract class PublicKeyCredentialSourceRepositoryBase implements PublicKeyCrede
$this->createInitialSchema($pdo);
}
$this->addNameColumn($pdo);
$this->addUserDnColumn($pdo);
}

/**
Expand Down Expand Up @@ -676,6 +709,29 @@ abstract class PublicKeyCredentialSourceRepositoryBase implements PublicKeyCrede
}
}

/**
* Adds the user DN column if not existing.
*
* @param PDO $pdo PDO
*/
protected function addUserDnColumn(PDO $pdo): void {
try {
$statement = $pdo->query("select * from pragma_table_info('" . $this->getTableName() . "') where name = 'userDN';");
$results = $statement->fetchAll();
if (empty($results)) {
$sql = 'alter table ' . $this->getTableName() . ' add column userDN VARCHAR(255);';
logNewMessage(LOG_DEBUG, $sql);
$pdo->exec($sql);
$sql = 'update ' . $this->getTableName() . ' set userDn = userId;';
logNewMessage(LOG_DEBUG, $sql);
$pdo->exec($sql);
}
}
catch (PDOException $e) {
logNewMessage(LOG_ERR, 'Unable to add userDN column to table: ' . $e->getMessage());
}
}

/**
* Exports all entries.
*
Expand All @@ -695,6 +751,7 @@ abstract class PublicKeyCredentialSourceRepositoryBase implements PublicKeyCrede
'registrationTime' => $dbRow['registrationTime'],
'lastUseTime' => $dbRow['lastUseTime'],
'name' => $dbRow['name'],
'userDN' => $dbRow['userDN'],
];
}
return $data;
Expand All @@ -713,14 +770,15 @@ abstract class PublicKeyCredentialSourceRepositoryBase implements PublicKeyCrede
return;
}
foreach ($data as $dbRow) {
$statement = $pdo->prepare('insert into ' . $this->getTableName() . ' (userId, credentialId, credentialSource, registrationTime, lastUseTime, name) VALUES (?, ?, ?, ?, ?, ?)');
$statement = $pdo->prepare('insert into ' . $this->getTableName() . ' (userId, credentialId, credentialSource, registrationTime, lastUseTime, name, userDN) VALUES (?, ?, ?, ?, ?, ?, ?)');
$statement->execute([
$dbRow['userId'],
$dbRow['credentialId'],
$dbRow['credentialSource'],
$dbRow['registrationTime'],
$dbRow['lastUseTime'],
$dbRow['name']
$dbRow['name'],
$dbRow['userDN'],
]);
}
}
Expand Down Expand Up @@ -835,6 +893,27 @@ class PublicKeyCredentialSourceRepositoryMySql extends PublicKeyCredentialSource
// added via initial schema
}

/**
* {@inheritDoc}
*/
protected function addUserDnColumn(PDO $pdo): void {
try {
$statement = $pdo->query("show columns from " . $this->getTableName() . " like 'userDN';");
$results = $statement->fetchAll();
if (empty($results)) {
$sql = 'alter table ' . $this->getTableName() . ' add column userDN VARCHAR(255);';
logNewMessage(LOG_DEBUG, $sql);
$pdo->exec($sql);
$sql = 'update ' . $this->getTableName() . ' set userDn = userId;';
logNewMessage(LOG_DEBUG, $sql);
$pdo->exec($sql);
}
}
catch (PDOException $e) {
logNewMessage(LOG_ERR, 'Unable to add userDN column to table: ' . $e->getMessage());
}
}

}


Loading
Loading