Skip to content

Commit

Permalink
feat: add LDAP adapter (dvsa/olcs-backend#210)
Browse files Browse the repository at this point in the history
* feat: add LDAP adapter

* chore: fix PHP CS issue

* chore: fix test

* chore: fix PHP CS

* fix: align config with `vol-app` compose.yaml
  • Loading branch information
JoshuaLicense authored Aug 14, 2024
1 parent 8055581 commit f1b64c0
Show file tree
Hide file tree
Showing 10 changed files with 1,146 additions and 80 deletions.
3 changes: 2 additions & 1 deletion app/api/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 app/api/composer.lock

Large diffs are not rendered by default.

51 changes: 50 additions & 1 deletion app/api/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 @@ -247,7 +251,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 @@ -259,7 +286,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 app/api/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 app/api/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 app/api/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 app/api/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);
}
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 app/api/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

0 comments on commit f1b64c0

Please sign in to comment.