Skip to content
This repository has been archived by the owner on Aug 16, 2024. It is now read-only.

feat: add LDAP adapter #210

Merged
merged 5 commits into from
Aug 14, 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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@
"mockery/mockery": "^1.6",
"johnkary/phpunit-speedtrap": "^4.0",
"mikey179/vfsstream": "^1.6",
"dms/phpunit-arraysubset-asserts": "^0.5.0"
"dms/phpunit-arraysubset-asserts": "^0.5.0",
"dvsa/authentication-ldap": "^3"
},
"extra" : {
"bamarni-bin": {
Expand Down
888 changes: 814 additions & 74 deletions composer.lock

Large diffs are not rendered by default.

51 changes: 50 additions & 1 deletion config/autoload/local.php.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<?php

use Dvsa\Contracts\Auth\OAuthClientInterface;
use Dvsa\Authentication\Cognito\Client as CognitoClient;
use Dvsa\Authentication\Ldap\Client as LdapClient;

/**
* Local Configuration Override
*
Expand Down Expand Up @@ -233,7 +237,30 @@ return [
'api_key' => '',
'proxy' => "",
],
/*
|--------------------------------------------------------------------------
| Authentication Identity Provider
|--------------------------------------------------------------------------
|
| Select an identity provider client that will be used to connect to an
| identity provider that implement the `OAuthClientInterface`.
|
| This config file is loaded after the module configuration, so overwriting
| the service manager alias in a global config file will overwrite the
| modules own config allowing this file to set the identity provider.
|
| Example adapters:
| - Dvsa\Authentication\Cognito\Client as CognitoClient
| - Dvsa\Authentication\Ldap\Client as LdapClient
|
*/
'service_manager' => [
'aliases' => [
OAuthClientInterface::class => LdapClient::class,
],
],
'auth' => [
'default_adapter' => 'ldap',
'adapters' => [
'cognito' => [
'adapter' => \Dvsa\Olcs\Auth\Adapter\CognitoAdapter::class,
Expand All @@ -245,7 +272,29 @@ return [
'proxy' => new \Laminas\Stdlib\ArrayUtils\MergeRemoveKey(),
],
],
'openam' => new \Laminas\Stdlib\ArrayUtils\MergeRemoveKey(),
/*
|--------------------------------------------------------------------------
| LDAP Local Credentials
|--------------------------------------------------------------------------
|
| The login credentials that match the OpenLDAP container bundled as
| part of https://github.com/dvsa/vol-docker-compose.
|
| These are default LOCAL values, do not alter unless defaults have changed.
|
*/
'ldap' => [
'adapter' => \Dvsa\Olcs\Auth\Adapter\LdapAdapter::class,
'host' => 'openldap',
'port' => 1389,
'admin_dn' => 'cn=admin,dc=vol,dc=dvsa',
'admin_password' => 'admin',
'rdn' => 'cn',
'object_class' => ['inetOrgPerson'],
'base_dn' => 'ou=users,dc=vol,dc=dvsa',
'encryption' => 'none',
'secret' => 'SUPER_SECRET',
],
],
],
'acquired_rights' => [
Expand Down
4 changes: 2 additions & 2 deletions module/Api/src/Rbac/JWTIdentityProviderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Dvsa\Olcs\Api\Rbac;

use Dvsa\Authentication\Cognito\Client;
use Dvsa\Contracts\Auth\OAuthClientInterface;
use Psr\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\FactoryInterface;

Expand All @@ -26,7 +26,7 @@ public function __invoke(ContainerInterface $container, $requestedName, array $o
return new JWTIdentityProvider(
$container->get('RepositoryServiceManager')->get('User'),
$container->get('Request'),
$container->get(Client::class)
$container->get(OAuthClientInterface::class)
);
}
}
5 changes: 5 additions & 0 deletions module/Auth/config/module.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

return [
'service_manager' => [
'aliases' => [
\Dvsa\Contracts\Auth\OAuthClientInterface::class => \Dvsa\Authentication\Cognito\Client::class,
],
'factories' => [
\Dvsa\Olcs\Auth\Service\AuthenticationServiceInterface::class => \Dvsa\Olcs\Auth\Service\AuthenticationServiceFactory::class,
\Laminas\Authentication\Adapter\ValidatableAdapterInterface::class => \Dvsa\Olcs\Auth\Adapter\ValidatableAdapterFactory::class,
\Dvsa\Authentication\Cognito\Client::class => \Dvsa\Olcs\Auth\Client\CognitoClientFactory::class,
\Dvsa\Olcs\Auth\Adapter\CognitoAdapter::class => \Dvsa\Olcs\Auth\Adapter\CognitoAdapterFactory::class,
\Dvsa\Olcs\Auth\Service\PasswordService::class => \Dvsa\Olcs\Auth\Service\PasswordServiceFactory::class,
\Dvsa\Authentication\Ldap\Client::class => \Dvsa\Olcs\Auth\Client\LdapClientFactory::class,
\Dvsa\Olcs\Auth\Adapter\LdapAdapter::class => \Dvsa\Olcs\Auth\Adapter\LdapAdapterFactory::class,
],
],
];
3 changes: 2 additions & 1 deletion module/Auth/src/Adapter/CognitoAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Dvsa\Contracts\Auth\AccessTokenInterface;
use Dvsa\Contracts\Auth\Exceptions\ChallengeException;
use Dvsa\Contracts\Auth\Exceptions\ClientException;
use Dvsa\Contracts\Auth\OAuthClientInterface;
use Dvsa\Contracts\Auth\Exceptions\InvalidTokenException;
use Dvsa\Contracts\Auth\ResourceOwnerInterface;
use Dvsa\Olcs\Auth\Exception\ResetPasswordException;
Expand Down Expand Up @@ -338,7 +339,7 @@ private function buildUserObject(AccessTokenInterface $token): array
$resourceOwner = $this->client->getResourceOwner($token);

return [
'Provider' => Client::class,
'Provider' => OAuthClientInterface::class,
'Token' => $token,
'ResourceOwner' => $resourceOwner,
'AccessToken' => $token->getToken(),
Expand Down
212 changes: 212 additions & 0 deletions module/Auth/src/Adapter/LdapAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<?php

declare(strict_types=1);

namespace Dvsa\Olcs\Auth\Adapter;

use Dvsa\Authentication\Ldap\Client;
use Dvsa\Contracts\Auth\AccessTokenInterface;
use Dvsa\Contracts\Auth\Exceptions\ChallengeException;
use Dvsa\Contracts\Auth\Exceptions\ClientException;
use Dvsa\Contracts\Auth\Exceptions\InvalidTokenException;
use Dvsa\Contracts\Auth\OAuthClientInterface;
use Dvsa\Contracts\Auth\ResourceOwnerInterface;
use Dvsa\Olcs\Auth\Exception\ResetPasswordException;
use Dvsa\Olcs\Transfer\Result\Auth\ChangeExpiredPasswordResult;
use Dvsa\Olcs\Transfer\Result\Auth\ChangePasswordResult;
use Dvsa\Olcs\Transfer\Result\Auth\DeleteUserResult;
use Laminas\Authentication\Adapter\AbstractAdapter;
use Laminas\Authentication\Result;
use Olcs\Logging\Log\Logger;

class LdapAdapter extends AbstractAdapter
{
protected Client $client;

public function __construct(Client $client)
{
$this->client = $client;
}

public function authenticate(): Result
{
try {
$token = $this->client->authenticate($this->getIdentity(), $this->getCredential());
return new Result(Result::SUCCESS, $this->buildUserObject($token));
} catch (InvalidTokenException | ClientException $e) {
Logger::err(
sprintf(
'There was an error attempting to login the user %s: %s',
$this->getIdentity(),
$e->getMessage()
),
$e->getTrace()
);
return new Result(Result::FAILURE, [], [$e->getMessage()]);
}
}

public function changePassword(string $identifier, string $previousPassword, string $newPassword): ChangePasswordResult
{
try {
$this->client->authenticate($identifier, $previousPassword);
} catch (ClientException $e) {
Logger::debug('LDAP client: change password ClientException checking previous password: ' . $e->getMessage());
return new ChangePasswordResult(ChangePasswordResult::FAILURE_CLIENT_ERROR, $e->getMessage());
} catch (ChallengeException $e) {
// Do nothing as this means the password was valid
}

if ($previousPassword === $newPassword) {
return new ChangePasswordResult(ChangePasswordResult::FAILURE_PASSWORD_REUSE, ChangePasswordResult::MESSAGE_PASSWORD_REUSE);
}

try {
$this->client->changePassword($identifier, $newPassword);
return new ChangePasswordResult(ChangePasswordResult::SUCCESS, ChangePasswordResult::MESSAGE_GENERIC_SUCCESS);
} catch (ClientException $e) {
Logger::debug('Cognito client: change password ClientException: ' . $e->getMessage());

return new ChangePasswordResult(ChangePasswordResult::FAILURE, ChangePasswordResult::MESSAGE_GENERIC_FAIL);
}
}

/**
* @throws ResetPasswordException
*/
public function resetPassword(string $identifier, string $newPassword): bool
{
try {
return $this->client->changePassword($identifier, $newPassword);
} catch (ClientException $e) {
Logger::debug('Ldap client: reset password ClientException: ' . $e->getMessage());
throw new ResetPasswordException($e->getMessage());
} catch (\Exception $e) {
Logger::err('Unknown reset password error from Cognito client: ' . $e->getMessage());
throw new ResetPasswordException($e->getMessage());
}
}

/**
* @throws ClientException
*/
public function register(string $identifier, string $password, string $email, array $attributes = []): void
{
$attributes = array_merge(['email' => $email], $attributes);
$this->client->register($identifier, $password, $attributes);
}

public function changeExpiredPassword(string $newPassword, string $challengeToken, string $username): ChangeExpiredPasswordResult
{
try {
$token = $this->client->authenticate($username, $newPassword);
} catch (ClientException $e) {
Logger::err('Ldap client: change password ClientException checking previous password: ' . $e->getMessage());
return new ChangeExpiredPasswordResult(ChangeExpiredPasswordResult::FAILURE_CLIENT_ERROR, [], [$e->getMessage()]);
}

return new ChangeExpiredPasswordResult(ChangeExpiredPasswordResult::SUCCESS, $this->buildUserObject($token));
}

public function refreshToken(string $refreshToken, string $identifier): Result
{
// No refresh token functionality in LDAP.
return new Result(Result::FAILURE, []);
}

/**
* @throws ClientException
*/
public function changeAttribute(string $identifier, string $key, string $value): void
{
$this->client->changeAttribute($identifier, $key, $value);
}

public function deleteUser(string $identifier): DeleteUserResult
{
throw new \RuntimeException('Not implemented');
}

/**
* @throws ClientException
*/
public function disableUser(string $identifier): void
{
$this->client->disableUser($identifier);
}

/**
* @throws ClientException
*/
public function enableUser(string $identifier): void
{
$this->client->enableUser($identifier);
}

/**
* @throws ClientException
*/
public function getUserByIdentifier(string $identifier): ResourceOwnerInterface
{
return $this->client->getUserByIdentifier($identifier);
}

/**
* @throws ClientException
*/
public function registerIfNotPresent(string $identifier, string $password, string $email, array $attributes = []): bool
{
if (!$this->doesUserExist($identifier)) {
$this->register($identifier, $password, $email, $attributes);
return true;
}
return false;
}

/**
* @throws ClientException
*/
public function doesUserExist(string $identifier): bool
{
try {
$this->getUserByIdentifier($identifier);
} catch (ClientException $e) {
throw $e;
}

return true;
}

/**
* @return mixed|string
*/
public function getIdentity()
{
$identity = parent::getIdentity();
if (!empty($identity) && is_string($identity)) {
$identity = strtolower($identity);
fibble marked this conversation as resolved.
Show resolved Hide resolved
}
return $identity;
}

/**
* @throws InvalidTokenException
*/
private function buildUserObject(AccessTokenInterface $token): array
{
$idTokenClaims = $this->client->decodeToken($token->getIdToken());
$accessTokenClaims = $this->client->decodeToken($token->getToken());
$resourceOwner = $this->client->getResourceOwner($token);

return [
'Provider' => OAuthClientInterface::class,
'Token' => $token,
'ResourceOwner' => $resourceOwner,
'AccessToken' => $token->getToken(),
'AccessTokenClaims' => $accessTokenClaims,
'IdToken' => $token->getIdToken(),
'IdTokenClaims' => $idTokenClaims,
'RefreshToken' => $token->getRefreshToken(),
];
}
}
19 changes: 19 additions & 0 deletions module/Auth/src/Adapter/LdapAdapterFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Dvsa\Olcs\Auth\Adapter;

use Dvsa\Authentication\Ldap\Client;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Psr\Container\ContainerInterface;

class LdapAdapterFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null): LdapAdapter
{
$client = $container->get(Client::class);

return new LdapAdapter($client);
}
}
Loading
Loading