diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5976415 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# App +composer.lock +vendor/* + +# Test +.phpunit* +test/storage/* +!test/storage/.gitkeep +public/coverage/* + +# Dev +.DS_Store +.nova/* diff --git a/README.md b/README.md index 441babc..61b36bf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,340 @@ -# auth -Auth +# Auth + +## Presentation + +The authentication service is intended to manage access (Applications and Users) to applications and APIs. + +The application wishing to use this service must first obtain an API key (to be generated). + +Authentication service aims to provide an access token allowing access of applications and users. + +Obtaining the access token is subject to various methods. + +The access token has a limited lifetime. +It embeds data relating to its applicant (application, user). + + +## Technologies used + +- `PHP 8.1` +- `Composer` for dependencies management (PHP) + + +## Installation + +`composer install` + + +## Request access + +For each use case the following setup is required. + +```php +use Phant\Auth\Domain\Service\AccessToken as ServiceAccessToken; +use Phant\Auth\Domain\Service\RequestAccess as ServiceRequestAccess; +use Phant\Auth\Domain\DataStructure\SslKey; +use App\RepositoryRequestAccess; + + +// Config + +$sslKey = new SslKey('private key', 'public key'); +$repositoryRequestAccess = new RepositoryRequestAccess(); + + +// Build services + +$serviceRequestAccess = new ServiceRequestAccess( + $repositoryRequestAccess, + $sslKey +); + +$serviceAccessToken = return new ServiceAccessToken( + $sslKey, + $serviceRequestAccess +) +``` + +### From API key + +Process : + +1. The application requests an access token by providing its API key, +2. The service provides an access token. + +```php +use Phant\Auth\Domain\Service\RequestAccessFromApiKey as ServiceRequestAccessFromApiKey; +use App\RepositoryApplication; + + +// Config + +$repositoryApplication = new RepositoryApplication(); + + +// Build services + +$serviceRequestAccessFromApiKey = new ServiceRequestAccessFromApiKey( + $serviceRequestAccess, + $serviceAccessToken, + $repositoryApplication +); + + +// Obtain API key from application + +/* @todo */ +$apiKey = 'XXXXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; + + +// Request access token + +$accessToken = $serviceRequestAccessFromApiKey->getAccessToken($apiKey); +``` + + +### From OTP + +Process : + +1. The application asks the user to authenticate himself by providing his contact details (last name, first name and e-mail address), +2. The application generates an access request by providing its identity and the user's contact details (last name, first name and e-mail address), +3. The service generates an OTP and requests its sending to the user, +4. The user receives an OTP, +5. The application retrieves the OTP from the user, +6. The user transmits the received OTP to the application, +7. The application verifies the OTP with the service, +8. The application requests an access token, +9. The service provides an access token. + +The OTP is sent to user by your own OtpSender service (e-mail, SMS, etc.). + +```php +use Phant\Auth\Domain\Service\RequestAccessFromOtp as ServiceRequestAccessFromOtp; +use Phant\Auth\Domain\DataStructure\Application; +use Phant\Auth\Domain\DataStructure\User; +use App\OtpSender; + + +// Config + +$repositoryApplication = new RepositoryApplication(); +$OtpSender = new OtpSender(); + + +// Build services + +$serviceRequestAccessFromOtp = new ServiceRequestAccessFromOtp( + $serviceRequestAccess, + $serviceAccessToken, + $OtpSender +); + + +// Request access token + +$user = new User( + 'john.doe@domain.ext', + 'John', + 'DOE' +); + +$application = new Application( + 'eb7c9c44-32c2-4e88-8410-4ebafb18fdf7', + 'My app', + 'https://domain.ext/image.ext' +); + +$requestAccessToken = $serviceRequestAccessFromOtp->generate($user, $application); + + +// Obtain OTP from user + +/* @todo */ +$otp = '123456'; + + +// Verify OTP + +$isValid = $serviceRequestAccessFromOtp->verify($otp); + +if ( ! $isValid) { + $numberOfAttemptsRemaining = $serviceRequestAccessFromOtp->numberOfAttemptsRemaining($requestAccessToken); +} + + +// Get access token + +$accessToken = $serviceRequestAccessFromOtp->getAccessToken($requestAccessToken); +``` + + +### From third party + +Process : + +1. The application generates an access request by providing its identity, +2. The service generates an Access-Request and returns an Access-Request Token, +3. The application forwards the authentication request to the third party service by passing the access request token, +4. The user authenticates with the third-party authentication service, +5. The application retrieves the user authentication result, +6. The application declares the authentication result, +7. The service takes note of the authentication. +8. The service provides an access token. + +```php +use Phant\Auth\Domain\Service\RequestAccessFromThirdParty as ServiceRequestAccessFromThirdParty; +use Phant\Auth\Domain\DataStructure\Application; +use Phant\Auth\Domain\DataStructure\User; +use App\RepositoryRequestAccess; + + +// Config + +$repositoryApplication = new RepositoryApplication(); +$OtpSender = new OtpSender(); + + +// Build services + +$serviceRequestAccessFromThirdParty = new ServiceRequestAccessFromThirdParty( + $serviceRequestAccess, + $serviceAccessToken +); + + +// Request access token + +$application = new Application( + 'eb7c9c44-32c2-4e88-8410-4ebafb18fdf7', + 'My app', + 'https://domain.ext/image.ext' +); + +$requestAccessToken = $serviceRequestAccessFromThirdParty->generate( + $application, + 'https://domain.ext/callback/url' +); + + +// Request third party auth with requestAccessToken + +/* @todo */ + + +// Obtain authentication status + +/* @todo */ +$isAuthorized = true; + + +// Obtain user data + +/* @todo */ +$user = new User( + 'john.doe@domain.ext', + 'John', + 'DOE' +); + + +// Set auth status + +$serviceRequestAccessFromThirdParty->setStatus($requestAccessToken, $user, $isAuthorized); + + +// Get access token + +$accessToken = $serviceRequestAccessFromThirdParty->getAccessToken($requestAccessToken); +``` + + +## Access token + +The access token is a JWT. + +For each use case the following setup is required. + +```php +use Phant\Auth\Domain\Service\AccessToken as ServiceAccessToken; +use Phant\Auth\Domain\Service\RequestAccess as ServiceRequestAccess; +use Phant\Auth\FixtDomainure\DataStructure\SslKey; +use App\RepositoryRequestAccess; + + +// Config + +$sslKey = new SslKey('private key', 'public key'); +$repositoryRequestAccess = new RepositoryRequestAccess(); + + +// Build services + +$serviceRequestAccess = new ServiceRequestAccess( + $repositoryRequestAccess, + $sslKey +); + +$serviceAccessToken = return new ServiceAccessToken( + $sslKey, + $serviceRequestAccess +) + + +// An access token + +$accessToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhcHAiOnsiaWQiOiJjMjI4MDI1OC03OGU5LTQ4ZmQtOTA1Zi0yYzhlMDIzYWNiOWMiLCJuYW1lIjoiRmxhc2hwb2ludCIsImxvZ28iOiJodHRwczpcL1wvdmlhLnBsYWNlaG9sZGVyLmNvbVwvNDAweDIwMD90ZXh0PUZsYXNocG9pbnQiLCJhcGlfa2V5IjoiZnc5TEFJcFkuclA2b2d5VlNRdEx1OWRWMXBqOTR2WG56ekVPNXNISldHeHdhNWMxZzZMa3owNlo5dGNuc21GNFNieVRqeURTaCJ9LCJ1c2VyIjp7ImVtYWlsX2FkZHJlc3MiOiJqb2huLmRvZUBkb21haW4uZXh0IiwibGFzdG5hbWUiOiJET0UiLCJmaXJzdG5hbWUiOiJKb2huIiwicm9sZSI6bnVsbH0sImlhdCI6MTY2MzY4NDM3MCwiZXhwIjoxNjYzNjk1MTcwfQ.a-wJ_T1ENG58zCw2X7oP2oZrziZRP_m0rOOkUkC2axAsx7O72ebGjQja-iry-lFvd1PF48BxejQw69LPUQKrx1Tb9oQ_8VqMhU97nR8Jd5v2jlWIA7CP2H9voQLE5ybHpqFO2IzgPf2MurzwXQ0tlSeiRbQzHLzMBbWhcQLU4aI'; +``` + +### JWT decrypt method + +The application may need the public key for the following uses : +- check the integrity of the token, +- extract data from the token. + +```php +use Phant\DataStructure\Token\Jwt; +use Phant\Error\NotCompliant; + +$publicKey = $serviceAccessToken->getPublicKey(); + +try { + $payLoad = (new Jwt($accessToken))->decode(publicKey); +} catch (NotCompliant $e) { + +} +``` + + +### Verification + +The application can verify the integrity of the token with the service. + +```php +use Phant\Auth\Domain\DataStructure\Application; + +$application = new Application( + 'eb7c9c44-32c2-4e88-8410-4ebafb18fdf7', + 'My app', + 'https://domain.ext/image.ext' +); + +$isValid = $serviceAccessToken->check($accessToken, $application); +``` + + +### Get user infos + +The app can get the token user data from the service. + +```php +use Phant\Auth\Domain\DataStructure\Application; + +$application = new Application( + 'eb7c9c44-32c2-4e88-8410-4ebafb18fdf7', + 'My app', + 'https://domain.ext/image.ext' +); + +$userInfos = $serviceAccessToken->getUserInfos($accessToken); +``` diff --git a/component/Domain/DataStructure/AccessToken.php b/component/Domain/DataStructure/AccessToken.php new file mode 100644 index 0000000..946590d --- /dev/null +++ b/component/Domain/DataStructure/AccessToken.php @@ -0,0 +1,94 @@ +value = $value; + $this->expire = new Expire(date('Y-m-d', time() + $lifetime)); + } + + public function getValue(): string + { + return $this->value; + } + + public function getExpire(): Expire + { + return $this->expire; + } + + public function __toString(): string + { + return $this->getValue(); + } + + public function check(SslKey $sslKey, Application $application): bool + { + try { + + $payload = (new Jwt($this->value))->decode($sslKey->getPublic()); + + $id = $payload[ self::PAYLOAD_KEY_APP ]->id ?? null; + + if (!$id) return false; + + if (!$application->isHisId($id)) return false; + + } catch (\Exception $e) { + return false; + } + + return true; + } + + public function getPayload(SslKey $sslKey): ?array + { + try { + + $payLoad = (new Jwt($this->value))->decode($sslKey->getPublic()); + + return $payLoad; + + } catch (\Exception $e) { + return null; + } + } + + public static function generate(SslKey $sslKey, Application $application, ?User $user, int $lifetime): self + { + $payload = [ + self::PAYLOAD_KEY_APP => SerializeApplication::serialize($application), + ]; + + if ($user) { + $payload[ self::PAYLOAD_KEY_USER ] = SerializeUser::serialize($user); + } + + return new self((string)Jwt::encode($sslKey->getPrivate(), $payload, $lifetime), $lifetime); + } +} diff --git a/component/Domain/DataStructure/AccessToken/Expire.php b/component/Domain/DataStructure/AccessToken/Expire.php new file mode 100644 index 0000000..d698c2a --- /dev/null +++ b/component/Domain/DataStructure/AccessToken/Expire.php @@ -0,0 +1,8 @@ +id = $id; + $this->name = $name; + $this->logo = $logo; + $this->apiKey = $apiKey; + } + + public function isHisApiKey(string|ApiKey $apiKey): bool + { + if (is_string($apiKey)) $apiKey = new ApiKey($apiKey); + + return ((string)$this->apiKey === (string)$apiKey); + } + + public function isHisId(string|Id $id): bool + { + if (is_string($id)) $id = new Id($id); + + return ((string)$this->id === (string)$id); + } +} diff --git a/component/Domain/DataStructure/Application/ApiKey.php b/component/Domain/DataStructure/Application/ApiKey.php new file mode 100644 index 0000000..af25c8a --- /dev/null +++ b/component/Domain/DataStructure/Application/ApiKey.php @@ -0,0 +1,31 @@ +value === (string)$apiKey; + } + + private static function generateRandomString($length = 10) { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + $charactersLength = strlen($characters); + $randomString = ''; + for ($i = 0; $i < $length; $i++) { + $randomString .= $characters[rand(0, $charactersLength - 1)]; + } + + return $randomString; + } +} diff --git a/component/Domain/DataStructure/Application/Collection.php b/component/Domain/DataStructure/Application/Collection.php new file mode 100644 index 0000000..3177d86 --- /dev/null +++ b/component/Domain/DataStructure/Application/Collection.php @@ -0,0 +1,40 @@ +itemsIterator() as $entity) { + if ($entity->isHisId($id)) return $entity; + } + + return null; + } + + public function searchByApiKey(string|ApiKey $apiKey): ?Application + { + if (is_string($apiKey)) $apiKey = new ApiKey($apiKey); + + foreach ($this->itemsIterator() as $entity) { + if ($entity->isHisApiKey($apiKey)) return $entity; + } + + return null; + } +} diff --git a/component/Domain/DataStructure/Application/Id.php b/component/Domain/DataStructure/Application/Id.php new file mode 100644 index 0000000..821fcdc --- /dev/null +++ b/component/Domain/DataStructure/Application/Id.php @@ -0,0 +1,8 @@ +id = $id; + $this->application = $application; + $this->user = $user; + $this->authMethod = $authMethod; + $this->state = $state; + $this->lifetime = $lifetime; + $this->expiration = time() + $lifetime; + } + + public function getId(): Id + { + return $this->id; + } + + public function setApplication(Application $application): void + { + if ($this->application) { + throw new NotAuthorized('It is not allowed to modify the application'); + } + + $this->application = $application; + } + + public function getApplication(): ?Application + { + return $this->application; + } + + public function setUser(User $user): void + { + if ($this->user) { + throw new NotAuthorized('It is not allowed to modify the user'); + } + + $this->user = $user; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function getAuthMethod(): AuthMethod + { + return $this->authMethod; + } + + public function canBeSetStateTo(State $state): bool + { + return $this->state->canBeSetTo($state); + } + + public function setState(State $state): self + { + if (!$this->state->canBeSetTo($state)) { + throw new NotAuthorized('State can be set to set to : ' . $state); + } + + $this->state = $state; + + return $this; + } + + public function getState(): State + { + return $this->state; + } + + public function getLifetime(): int + { + return $this->lifetime; + } + + public function getExpiration(): int + { + return $this->expiration; + } + + public function tokenizeId(SslKey $sslKey): Token + { + $id = (string)$this->id; + + $datas = json_encode([ + self::TOKEN_PAYLOAD_LIFETIME => $this->lifetime, + self::TOKEN_PAYLOAD_EXPIRATION => $this->expiration, + self::TOKEN_PAYLOAD_ID => $id, + ]); + + $token = $sslKey->encrypt($datas); + + $token = strtr(base64_encode($token), '+/=', '._-'); + + return new Token($token); + } + + public static function untokenizeId(Token $token, SslKey $sslKey): Id + { + $token = base64_decode(strtr((string)$token, '._-', '+/=')); + + $datas = $sslKey->decrypt($token); + + $datas = json_decode($datas, true); + + $expiration = $datas[ self::TOKEN_PAYLOAD_EXPIRATION ] ?? 0; + if ($expiration < time()) { + throw new NotCompliant('Token expired'); + } + + $id = $datas[ self::TOKEN_PAYLOAD_ID ]; + + return new Id($id); + } +} diff --git a/component/Domain/DataStructure/RequestAccess/AuthMethod.php b/component/Domain/DataStructure/RequestAccess/AuthMethod.php new file mode 100644 index 0000000..fad7350 --- /dev/null +++ b/component/Domain/DataStructure/RequestAccess/AuthMethod.php @@ -0,0 +1,22 @@ + 'API key', + self::OTP => 'OTP', + self::THIRD_PARTY => 'Third party', + ]; + + public function is(string|self $authMethod): bool + { + return ($this->value == (string)$authMethod); + } +} diff --git a/component/Domain/DataStructure/RequestAccess/CallbackUrl.php b/component/Domain/DataStructure/RequestAccess/CallbackUrl.php new file mode 100644 index 0000000..15c1007 --- /dev/null +++ b/component/Domain/DataStructure/RequestAccess/CallbackUrl.php @@ -0,0 +1,8 @@ +value === (string)$otp; + } + + public static function generate(): self + { + $otp = ''; + + $characters = '0123456789'; + for ($i = 0; $i < self::LENGTH; $i++) { + $otp.= $characters[mt_rand(0, strlen($characters) - 1)]; + } + + return new self($otp); + } +} diff --git a/component/Domain/DataStructure/RequestAccess/State.php b/component/Domain/DataStructure/RequestAccess/State.php new file mode 100644 index 0000000..4219766 --- /dev/null +++ b/component/Domain/DataStructure/RequestAccess/State.php @@ -0,0 +1,45 @@ + 'Requested', + self::REFUSED => 'Refused', + self::VERIFIED => 'Verified', + self::GRANTED => 'Granted', + ]; + + public function canBeSetTo(string|self $state): bool + { + if (is_string($state)) $state = new self($state); + + switch ($state->getValue()) { + case State::REQUESTED : + + break; + + case State::REFUSED : + + return ($this->value == State::REQUESTED); + + case State::VERIFIED : + + return ($this->value == State::REQUESTED); + + case State::GRANTED : + + return ($this->value == State::VERIFIED); + + } + + return false; + } +} diff --git a/component/Domain/DataStructure/RequestAccess/Token.php b/component/Domain/DataStructure/RequestAccess/Token.php new file mode 100644 index 0000000..30263ff --- /dev/null +++ b/component/Domain/DataStructure/RequestAccess/Token.php @@ -0,0 +1,8 @@ +apiKey = $apiKey; + } +} diff --git a/component/Domain/DataStructure/RequestAccessFromOtp.php b/component/Domain/DataStructure/RequestAccessFromOtp.php new file mode 100644 index 0000000..dd0279c --- /dev/null +++ b/component/Domain/DataStructure/RequestAccessFromOtp.php @@ -0,0 +1,71 @@ +otp = Otp::generate(); + $this->numberOfRemainingAttempts = $numberOfAttemptsLimit; + } + + public function getOtp(): Otp + { + return $this->otp; + } + + public function getNumberOfRemainingAttempts(): int + { + return $this->numberOfRemainingAttempts; + } + + public function checkOtp(string|Otp $otp): bool + { + if ($this->numberOfRemainingAttempts <= 0) { + throw new NotAuthorized('The number of attempts is reach'); + } + + if (is_string($otp)) $otp = new Otp($otp); + + $this->numberOfRemainingAttempts--; + + return $this->otp->check($otp); + } +} diff --git a/component/Domain/DataStructure/RequestAccessFromThirdParty.php b/component/Domain/DataStructure/RequestAccessFromThirdParty.php new file mode 100644 index 0000000..7c9ef38 --- /dev/null +++ b/component/Domain/DataStructure/RequestAccessFromThirdParty.php @@ -0,0 +1,43 @@ +callbackUrl = $callbackUrl; + } + + public function getCallbackUrl(): CallbackUrl + { + return $this->callbackUrl; + } +} diff --git a/component/Domain/DataStructure/SslKey.php b/component/Domain/DataStructure/SslKey.php new file mode 100644 index 0000000..0ff57a1 --- /dev/null +++ b/component/Domain/DataStructure/SslKey.php @@ -0,0 +1,66 @@ +private = $private; + $this->public = $public; + } + + public function getPrivate(): string + { + return $this->private; + } + + public function getPublic(): string + { + return $this->public; + } + + public function encrypt(string $data): string + { + try { + $success = openssl_private_encrypt( + $data, + $encryptedData, + $this->private + ); + } catch (\Exception $e) { + $success = false; + } + + if (!$success) { + throw new NotCompliant('Encryption invalid, verify private key'); + } + + return $encryptedData; + } + + public function decrypt(string $encryptedData): string + { + try { + $success = openssl_public_decrypt( + $encryptedData, + $data, + $this->public + ); + } catch (\Exception $e) { + $success = false; + } + + if (!$success) { + throw new NotCompliant('Decryption invalid, verify encrypted data or public key'); + } + + return $data; + } +} diff --git a/component/Domain/DataStructure/User.php b/component/Domain/DataStructure/User.php new file mode 100644 index 0000000..125ee5c --- /dev/null +++ b/component/Domain/DataStructure/User.php @@ -0,0 +1,36 @@ +emailAddress = $emailAddress; + $this->lastname = $lastname; + $this->firstname = $firstname; + $this->role = $role; + } +} diff --git a/component/Domain/DataStructure/User/EmailAddress.php b/component/Domain/DataStructure/User/EmailAddress.php new file mode 100644 index 0000000..920fbc8 --- /dev/null +++ b/component/Domain/DataStructure/User/EmailAddress.php @@ -0,0 +1,8 @@ + (string) $accessToken->getValue(), + 'expire' => (string) $accessToken->getExpire()->getUtc(), + ]; + } +} diff --git a/component/Domain/Serialize/Application.php b/component/Domain/Serialize/Application.php new file mode 100644 index 0000000..9c25cd6 --- /dev/null +++ b/component/Domain/Serialize/Application.php @@ -0,0 +1,19 @@ + (string) $application->id, + 'name' => (string) $application->name, + 'logo' => $application->logo ? (string) $application->logo : null, + 'api_key' => (string) $application->apiKey, + ]; + } +} diff --git a/component/Domain/Serialize/User.php b/component/Domain/Serialize/User.php new file mode 100644 index 0000000..d33231e --- /dev/null +++ b/component/Domain/Serialize/User.php @@ -0,0 +1,19 @@ + (string)$user->emailAddress, + 'lastname' => $user->lastname ? (string)$user->lastname : null, + 'firstname' => $user->firstname ? (string)$user->firstname : null, + 'role' => $user->role ? (string)$user->role : null, + ]; + } +} diff --git a/component/Domain/Service/AccessToken.php b/component/Domain/Service/AccessToken.php new file mode 100644 index 0000000..7b80fe8 --- /dev/null +++ b/component/Domain/Service/AccessToken.php @@ -0,0 +1,79 @@ +sslKey = $sslKey; + $this->serviceRequestAccess = $serviceRequestAccess; + } + + public function getPublicKey(): string + { + return $this->sslKey->getPublic(); + } + + public function check(string $accessToken, Application $application, int $lifetime = self::LIFETIME): bool + { + return (new EntityAccessToken($accessToken, $lifetime))->check( + $this->sslKey, + $application + ); + } + + public function getFromToken(RequestAccess $requestAccess, int $lifetime = self::LIFETIME): EntityAccessToken + { + // Check request access status + if (!$requestAccess->canBeSetStateTo(new State(State::GRANTED))) { + throw new NotAuthorized('The access request is invalid'); + } + + // Generate new access token + $accessToken = EntityAccessToken::generate( + $this->sslKey, + $requestAccess->getApplication(), + $requestAccess->getUser(), + $lifetime + ); + + // Change state + $this->serviceRequestAccess->set( + $requestAccess + ->setState(new State(State::GRANTED)) + ); + + return $accessToken; + } + + public function getUserInfos(string $accessToken, int $lifetime = self::LIFETIME): ?array + { + $payLoad = (new EntityAccessToken($accessToken, $lifetime))->getPayload($this->sslKey); + + if (!isset($payLoad[ EntityAccessToken::PAYLOAD_KEY_USER ])) return null; + + return (array)$payLoad[ EntityAccessToken::PAYLOAD_KEY_USER ]; + } +} diff --git a/component/Domain/Service/Application.php b/component/Domain/Service/Application.php new file mode 100644 index 0000000..f3b70a5 --- /dev/null +++ b/component/Domain/Service/Application.php @@ -0,0 +1,59 @@ +repository = $repository; + } + + public function add(string $name, ?string $logo = null): EntityApplication + { + $application = new EntityApplication( + Id::generate(), + new Name($name), + $logo ? new Logo($logo) : null, + ApiKey::generate() + ); + + $this->repository->set($application); + + return $application; + } + + public function set(EntityApplication $application): void + { + $this->repository->set($application); + } + + public function get(string|Id $id): EntityApplication + { + if (is_string($id)) $id = new Id($id); + + return $this->repository->get($id); + } + + public function getFromApiKey(string|ApiKey $apiKey): EntityApplication + { + if (is_string($apiKey)) $apiKey = new ApiKey($apiKey); + + return $this->repository->getFromApiKey($apiKey); + } +} diff --git a/component/Domain/Service/RequestAccess.php b/component/Domain/Service/RequestAccess.php new file mode 100644 index 0000000..bdf63ae --- /dev/null +++ b/component/Domain/Service/RequestAccess.php @@ -0,0 +1,56 @@ +repository = $repository; + $this->sslKey = $sslKey; + } + + public function set(EntityRequestAccess $requestAccess): void + { + $this->repository->set($requestAccess); + } + + public function get(string|Id $id): EntityRequestAccess + { + if (is_string($id)) $id = new Id($id); + + return $this->repository->get($id); + } + + public function getToken(EntityRequestAccess $requestAccess): Token + { + return $requestAccess->tokenizeId($this->sslKey); + } + + public function getFromToken(string|Token $token): EntityRequestAccess + { + if (is_string($token)) $token = new Id($token); + + $id = EntityRequestAccess::untokenizeId($token, $this->sslKey); + + return $this->get($id); + } +} diff --git a/component/Domain/Service/RequestAccessFromApiKey.php b/component/Domain/Service/RequestAccessFromApiKey.php new file mode 100644 index 0000000..8ae2513 --- /dev/null +++ b/component/Domain/Service/RequestAccessFromApiKey.php @@ -0,0 +1,74 @@ +serviceRequestAccess = $serviceRequestAccess; + $this->serviceAccessToken = $serviceAccessToken; + $this->repositoryApplication = $repositoryApplication; + } + + public function getAccessToken(string|ApiKey $apiKey, int $lifetime = self::LIFETIME): ?AccessToken + { + if (is_string($apiKey)) $apiKey = new ApiKey($apiKey); + + $requestAccess = $this->build($apiKey, $lifetime); + + $application = $this->repositoryApplication->getFromApiKey($apiKey); + + if ( ! $application) return null; + + if ( ! $application->isHisApiKey($apiKey)) return null; + + $requestAccess->setApplication($application); + + $requestAccess->setState(new State(State::VERIFIED)); + + $this->serviceRequestAccess->set($requestAccess); + + $accessToken = $this->serviceAccessToken->getFromToken($requestAccess); + + return $accessToken; + } + + private function build(ApiKey $apiKey, int $lifetime): EntityRequestAccessFromApiKey + { + return new EntityRequestAccessFromApiKey( + $apiKey, + $lifetime + ); + } +} diff --git a/component/Domain/Service/RequestAccessFromOtp.php b/component/Domain/Service/RequestAccessFromOtp.php new file mode 100644 index 0000000..198aa57 --- /dev/null +++ b/component/Domain/Service/RequestAccessFromOtp.php @@ -0,0 +1,108 @@ +serviceRequestAccess = $serviceRequestAccess; + $this->serviceAccessToken = $serviceAccessToken; + $this->otpSender = $OtpSender; + } + + public function generate(Application $application, User $user, int $numberOfAttemptsLimit = 3, int $lifetime = self::LIFETIME): Token + { + $requestAccess = $this->build($application, $user, $numberOfAttemptsLimit, $lifetime); + + $requestAccessToken = $this->serviceRequestAccess->getToken($requestAccess); + + $this->otpSender->send($requestAccessToken, $requestAccess, $requestAccess->getOtp()); + + $this->serviceRequestAccess->set($requestAccess); + + return $requestAccessToken; + } + + public function verify(string|Token $requestAccessToken, string|Otp $otp): bool + { + if (is_string($requestAccessToken)) $requestAccessToken = new Token($requestAccessToken); + + $requestAccess = $this->serviceRequestAccess->getFromToken($requestAccessToken); + + if (is_string($otp)) $otp = new Otp($otp); + + if ( ! $requestAccess->checkOtp($otp)) { + + $requestAccess->setState(new State(State::REFUSED)); + + return false; + } + + $requestAccess->setState(new State(State::VERIFIED)); + + $this->serviceRequestAccess->set($requestAccess); + + return true; + } + + public function getNumberOfRemainingAttempts(string|Token $requestAccessToken): int + { + if (is_string($requestAccessToken)) $requestAccessToken = new Token($requestAccessToken); + + $requestAccess = $this->serviceRequestAccess->getFromToken($requestAccessToken); + + return $requestAccess->getNumberOfRemainingAttempts($requestAccess); + } + + public function getAccessToken(string|Token $requestAccessToken): ?AccessToken + { + if (is_string($requestAccessToken)) $requestAccessToken = new Token($requestAccessToken); + + $requestAccess = $this->serviceRequestAccess->getFromToken($requestAccessToken); + + $accessToken = $this->serviceAccessToken->getFromToken($requestAccess); + + return $accessToken; + } + + private function build(Application $application, User $user, int $numberOfAttemptsLimit, int $lifetime): EntityRequestAccessFromOtp + { + return new EntityRequestAccessFromOtp( + $application, + $user, + $numberOfAttemptsLimit, + $lifetime + ); + } +} diff --git a/component/Domain/Service/RequestAccessFromThirdParty.php b/component/Domain/Service/RequestAccessFromThirdParty.php new file mode 100644 index 0000000..f3d7632 --- /dev/null +++ b/component/Domain/Service/RequestAccessFromThirdParty.php @@ -0,0 +1,93 @@ +serviceRequestAccess = $serviceRequestAccess; + $this->serviceAccessToken = $serviceAccessToken; + } + + public function generate(Application $application, string|CallbackUrl $callbackUrl, int $lifetime = self::LIFETIME): Token + { + if (is_string($callbackUrl)) $callbackUrl = new CallbackUrl($callbackUrl); + + $requestAccess = $this->build($application, $callbackUrl, $lifetime); + + $requestAccessToken = $this->serviceRequestAccess->getToken($requestAccess); + + $this->serviceRequestAccess->set($requestAccess); + + return $requestAccessToken; + } + + public function setStatus(string|Token $requestAccessToken, User $user, bool $isAuthorized): void + { + if (is_string($requestAccessToken)) $requestAccessToken = new Token($requestAccessToken); + + $requestAccess = $this->serviceRequestAccess->getFromToken($requestAccessToken); + + $requestAccess->setUser($user); + $requestAccess->setState(new State($isAuthorized ? State::VERIFIED : State::REFUSED)); + + $this->serviceRequestAccess->set($requestAccess); + } + + public function getCallbackUrl(string|Token $requestAccessToken): CallbackUrl + { + if (is_string($requestAccessToken)) $requestAccessToken = new Token($requestAccessToken); + + $requestAccess = $this->serviceRequestAccess->getFromToken($requestAccessToken); + + return $requestAccess->getCallbackUrl(); + } + + public function getAccessToken(string|Token $requestAccessToken): ?AccessToken + { + if (is_string($requestAccessToken)) $requestAccessToken = new Token($requestAccessToken); + + $requestAccess = $this->serviceRequestAccess->getFromToken($requestAccessToken); + + $accessToken = $this->serviceAccessToken->getFromToken($requestAccess); + + return $accessToken; + } + + private function build(Application $application, CallbackUrl $callbackUrl, int $lifetime): EntityRequestAccessFromThirdParty + { + return new EntityRequestAccessFromThirdParty( + $application, + $callbackUrl, + $lifetime + ); + } +} diff --git a/component/Fixture/DataStructure/AccessToken.php b/component/Fixture/DataStructure/AccessToken.php new file mode 100644 index 0000000..a20be1a --- /dev/null +++ b/component/Fixture/DataStructure/AccessToken.php @@ -0,0 +1,35 @@ + 'c2280258-78e9-48fd-905f-2c8e023acb9c', + 'name' => 'Flashpoint', + 'logo' => 'https://via.placeholder.com/400x200?text=Flashpoint', + 'api_key' => 'fw9LAIpY.rP6ogyVSQtLu9dV1pj94vXnzzEO5sHJWGxwa5c1g6Lkz06Z9tcnsmF4SbyTjyDSh', + ], + [ + 'id' => 'fa9a72a5-5c21-4056-818a-66b26626874a', + 'name' => 'LiveTube', + 'logo' => 'https://via.placeholder.com/400x200?text=LiveTube', + 'api_key' => 'kMgyGJlO.H6pYDtv8E51rK9D0gDSTYknvM7oEGgLL3Lbekj3EqWsMpDSz0Oo6ri5l7mDVnpkE', + ], + [ + 'id' => '1fa914ab-5d34-48d5-82fd-fc364c62b0f9', + 'name' => 'Taskeo', + 'logo' => 'https://via.placeholder.com/400x200?text=Taskeo', + 'api_key' => 'R2FpV3FU.w5skjmLm4VDylSrH1cSu7EQfrHVkIB5vacBF63Ni5WI8sIKAIQ2WJqISx7sl4TlJ', + ], + ]; + + public static function get(): EntityApplication + { + $datas = self::DATAS[0]; + + return self::buildFromDatas($datas); + } + + public static function getCollection(): Collection + { + $collection = new Collection(); + + foreach (self::DATAS as $datas) { + $collection->addApplication( + self::buildFromDatas($datas) + ); + } + + return $collection; + } + + private static function buildFromDatas(array $datas): EntityApplication + { + return new EntityApplication( + new Id($datas['id']), + new Name($datas['name']), + new Logo($datas['logo']), + new ApiKey($datas['api_key']) + ); + } +} diff --git a/component/Fixture/DataStructure/RequestAccessFromApiKey.php b/component/Fixture/DataStructure/RequestAccessFromApiKey.php new file mode 100644 index 0000000..c72a2f3 --- /dev/null +++ b/component/Fixture/DataStructure/RequestAccessFromApiKey.php @@ -0,0 +1,34 @@ +apiKey, + $lifetime + ); + } + + public static function getExpired(?State $state = null): EntityRequestAccessFromApiKey + { + return self::get($state, -9999); + } + + public static function getVerified(): EntityRequestAccessFromApiKey + { + return (self::get()) + ->setApplication(FixtureApplication::get()) + ->setState(new State(State::VERIFIED)); + } +} diff --git a/component/Fixture/DataStructure/RequestAccessFromOtp.php b/component/Fixture/DataStructure/RequestAccessFromOtp.php new file mode 100644 index 0000000..5db7a3d --- /dev/null +++ b/component/Fixture/DataStructure/RequestAccessFromOtp.php @@ -0,0 +1,39 @@ +setState(new State(State::VERIFIED)); + } +} diff --git a/component/Fixture/DataStructure/RequestAccessFromThirdParty.php b/component/Fixture/DataStructure/RequestAccessFromThirdParty.php new file mode 100644 index 0000000..3622053 --- /dev/null +++ b/component/Fixture/DataStructure/RequestAccessFromThirdParty.php @@ -0,0 +1,39 @@ +setUser(FixtureUser::get()) + ->setState(new State(State::VERIFIED)); + } +} diff --git a/component/Fixture/DataStructure/SslKey.php b/component/Fixture/DataStructure/SslKey.php new file mode 100644 index 0000000..5f2a561 --- /dev/null +++ b/component/Fixture/DataStructure/SslKey.php @@ -0,0 +1,79 @@ +cache = $cache; + } + + public function set(EntityApplication $application): void + { + $this->cache->set((string)$application->id, $application); + } + + public function get(Id $id): EntityApplication + { + $entity = $this->cache->get((string)$id); + if ($entity) return $entity; + + foreach (FixtureApplication::getCollection()->itemsIterator() as $entity) { + if ((string)$entity->id != (string)$id) continue; + + return $entity; + } + + throw new NotFound('Application not found from Id : ' . $id); + } + + public function getFromApiKey(ApiKey $apiKey): EntityApplication + { + foreach (FixtureApplication::getCollection()->itemsIterator() as $entity) { + if ((string)$entity->apiKey != (string)$apiKey) continue; + + return $entity; + } + + throw new NotFound('Application not found from API key : ' . $apiKey); + } + + public function getList(): Collection + { + return FixtureApplication::getCollection(); + } +} diff --git a/component/Fixture/Port/OtpSender.php b/component/Fixture/Port/OtpSender.php new file mode 100644 index 0000000..54802bd --- /dev/null +++ b/component/Fixture/Port/OtpSender.php @@ -0,0 +1,26 @@ +cache = $cache; + } + + public function send(Token $requestAccessToken, RequestAccess $requestAccess, Otp $otp): void + { + $this->cache->set((string)$requestAccessToken, $otp); + } +} diff --git a/component/Fixture/Port/RequestAccess.php b/component/Fixture/Port/RequestAccess.php new file mode 100644 index 0000000..7b3c35e --- /dev/null +++ b/component/Fixture/Port/RequestAccess.php @@ -0,0 +1,41 @@ +cache = $cache; + } + + public function set(EntityRequestAccess $requestAccess): void + { + $this->cache->set((string)$requestAccess->getId(), $requestAccess); + } + + public function get(Id $id): EntityRequestAccess + { + $entity = $this->cache->get((string)$id); + if ($entity) return $entity; + + $entity = FixtureRequestAccessFromOtp::get(); + + if ((string)$entity->getId() == (string)$id) { + throw $entity; + } + + throw new NotFound('Request access not found from Id : ' . $id); + } +} diff --git a/component/Fixture/Service/AccessToken.php b/component/Fixture/Service/AccessToken.php new file mode 100644 index 0000000..2705231 --- /dev/null +++ b/component/Fixture/Service/AccessToken.php @@ -0,0 +1,20 @@ +=8.1", + "phant/data-structure": "3.*" + }, + "require-dev": { + "psr/simple-cache": "^3.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.5", + "phant/cache": "1.*" + }, + "scripts": { + "analyse": "vendor/bin/phpstan analyse component --memory-limit=4G", + "test": "vendor/bin/phpunit test --testdox", + "code-coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit test --coverage-html public/coverage/html" + }, + "autoload": { + "psr-4": { + "Phant\\Auth\\": "component/" + } + }, + "autoload-dev": { + "psr-4": { + "Test\\": "test/" + } + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..ed1ffac --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,15 @@ + + + + test + + + + + + + + component/Domain + + + \ No newline at end of file diff --git a/test/Domain/DataStructure/AccessTokenTest.php b/test/Domain/DataStructure/AccessTokenTest.php new file mode 100644 index 0000000..ef77325 --- /dev/null +++ b/test/Domain/DataStructure/AccessTokenTest.php @@ -0,0 +1,95 @@ +fixture = FixtureAccessToken::get(); + } + + public function testConstruct(): void + { + $entity = new AccessToken($this->fixture->getValue(), 86400); + + $this->assertIsObject($entity); + $this->assertInstanceOf(AccessToken::class, $entity); + } + + public function testGetExpire(): void + { + $value = $this->fixture->getExpire(); + + $this->assertIsObject($value); + $this->assertInstanceOf(Expire::class, $value); + } + + public function testCheck(): void + { + $result = $this->fixture->check( + FixtureSslKey::get(), + FixtureApplication::get() + ); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + } + + public function testCheckInvalid(): void + { + $result = $this->fixture->check( + FixtureSslKey::getInvalid(), + FixtureApplication::get() + ); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + } + + public function testGetPayload(): void + { + $result = $this->fixture->getPayload( + FixtureSslKey::get() + ); + + $this->assertIsArray($result); + $this->assertArrayHasKey(AccessToken::PAYLOAD_KEY_APP, $result); + $this->assertArrayHasKey(AccessToken::PAYLOAD_KEY_USER, $result); + } + + public function testGetPayloadInvalid(): void + { + $result = $this->fixture->getPayload( + FixtureSslKey::getInvalid() + ); + + $this->assertNull($result); + } + + public function testGenerate(): void + { + $entity = AccessToken::generate( + FixtureSslKey::get(), + FixtureApplication::get(), + FixtureUser::get(), + 86400 + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(AccessToken::class, $entity); + } +} diff --git a/test/Domain/DataStructure/Application/ApiKeyTest.php b/test/Domain/DataStructure/Application/ApiKeyTest.php new file mode 100644 index 0000000..31caabd --- /dev/null +++ b/test/Domain/DataStructure/Application/ApiKeyTest.php @@ -0,0 +1,35 @@ +assertIsObject($value); + $this->assertInstanceOf(ApiKey::class, $value); + } + + public function testCheck(): void + { + $value = ApiKey::generate(); + $result = $value->check($value); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + } + + public function testCheckInvalid(): void + { + $value = ApiKey::generate(); + $result = $value->check(ApiKey::generate()); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + } +} diff --git a/test/Domain/DataStructure/Application/CollectionTest.php b/test/Domain/DataStructure/Application/CollectionTest.php new file mode 100644 index 0000000..d0db244 --- /dev/null +++ b/test/Domain/DataStructure/Application/CollectionTest.php @@ -0,0 +1,63 @@ +assertEquals(0, $collection->getNbItems()); + + $collection->addApplication( + FixtureApplication::get() + ); + + $this->assertEquals(1, $collection->getNbItems()); + } + + public function testSearchById(): void + { + $collection = FixtureApplication::getCollection(); + + $result = $collection->searchById( + FixtureApplication::get()->id + ); + + $this->assertIsObject($result); + + $collection = FixtureApplication::getCollection(); + + $result = $collection->searchById( + '1b3f18e5-c12d-4063-bf27-d77c2558ea1a' + ); + + $this->assertNull($result); + } + + public function testSearchByApiKey(): void + { + $collection = FixtureApplication::getCollection(); + + $result = $collection->searchByApiKey( + FixtureApplication::get()->apiKey + ); + + $this->assertIsObject($result); + + $collection = FixtureApplication::getCollection(); + + $result = $collection->searchByApiKey( + 'XXXXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' + ); + + $this->assertNull($result); + } +} diff --git a/test/Domain/DataStructure/Application/NameTest.php b/test/Domain/DataStructure/Application/NameTest.php new file mode 100644 index 0000000..b7377b5 --- /dev/null +++ b/test/Domain/DataStructure/Application/NameTest.php @@ -0,0 +1,26 @@ +assertIsObject($result); + $this->assertInstanceOf(Name::class, $result); + } + + public function testConstructFail(): void + { + $this->expectException(NotCompliant::class); + + new Name('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'); + } +} diff --git a/test/Domain/DataStructure/ApplicationTest.php b/test/Domain/DataStructure/ApplicationTest.php new file mode 100644 index 0000000..cf5c6d8 --- /dev/null +++ b/test/Domain/DataStructure/ApplicationTest.php @@ -0,0 +1,77 @@ +fixture = FixtureApplication::get(); + } + + public function testConstruct(): void + { + $entity = new Application( + Id::generate(), + new Name('Foo bar'), + new Logo('https://domain.ext/file.ext'), + ApiKey::generate() + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(Application::class, $entity); + } + + public function testCheck(): void + { + $result = $this->fixture->isHisApiKey( + $this->fixture->apiKey + ); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + } + + public function testCheckInvalid(): void + { + $result = $this->fixture->isHisApiKey( + ApiKey::generate() + ); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + } + + public function testIsHisId(): void + { + $result = $this->fixture->isHisId( + $this->fixture->id + ); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + } + + public function testIsHisIdInvalid(): void + { + $result = $this->fixture->isHisId( + Id::generate() + ); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + } +} diff --git a/test/Domain/DataStructure/RequestAccess/AuthMethodTest.php b/test/Domain/DataStructure/RequestAccess/AuthMethodTest.php new file mode 100644 index 0000000..6b98c77 --- /dev/null +++ b/test/Domain/DataStructure/RequestAccess/AuthMethodTest.php @@ -0,0 +1,25 @@ +is(AuthMethod::API_KEY); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + } + + public function testIsDifferent(): void + { + $result = (new AuthMethod(AuthMethod::API_KEY))->is(AuthMethod::OTP); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + } +} diff --git a/test/Domain/DataStructure/RequestAccess/OtpTest.php b/test/Domain/DataStructure/RequestAccess/OtpTest.php new file mode 100644 index 0000000..b45a64e --- /dev/null +++ b/test/Domain/DataStructure/RequestAccess/OtpTest.php @@ -0,0 +1,50 @@ +assertIsObject($result); + $this->assertInstanceOf(Otp::class, $result); + } + + public function testConstructFail(): void + { + $this->expectException(NotCompliant::class); + + new Otp('I23456'); + } + + public function testCheck(): void + { + $result = (new Otp('123456'))->check('123456'); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + } + + public function testCheckDifferent(): void + { + $result = (new Otp('123456'))->check('I23456'); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + } + + public function testGenerate(): void + { + $result = Otp::generate(); + + $this->assertIsObject($result); + $this->assertInstanceOf(Otp::class, $result); + } +} diff --git a/test/Domain/DataStructure/RequestAccess/StateTest.php b/test/Domain/DataStructure/RequestAccess/StateTest.php new file mode 100644 index 0000000..e8ada3b --- /dev/null +++ b/test/Domain/DataStructure/RequestAccess/StateTest.php @@ -0,0 +1,127 @@ +canBeSetTo(State::REQUESTED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + $result = (new State(State::REQUESTED)) + ->canBeSetTo(State::REFUSED); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + + + $result = (new State(State::REQUESTED)) + ->canBeSetTo(State::VERIFIED); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + + + $result = (new State(State::REQUESTED)) + ->canBeSetTo(State::GRANTED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + // Refused to ... + $result = (new State(State::REFUSED)) + ->canBeSetTo(State::REQUESTED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + $result = (new State(State::REFUSED)) + ->canBeSetTo(State::REFUSED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + $result = (new State(State::REFUSED)) + ->canBeSetTo(State::VERIFIED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + $result = (new State(State::REFUSED)) + ->canBeSetTo(State::GRANTED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + // Verified to ... + $result = (new State(State::VERIFIED)) + ->canBeSetTo(State::REQUESTED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + $result = (new State(State::VERIFIED)) + ->canBeSetTo(State::REFUSED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + $result = (new State(State::VERIFIED)) + ->canBeSetTo(State::VERIFIED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + $result = (new State(State::VERIFIED)) + ->canBeSetTo(State::GRANTED); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + + + // Granted to ... + $result = (new State(State::GRANTED)) + ->canBeSetTo(State::REQUESTED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + $result = (new State(State::GRANTED)) + ->canBeSetTo(State::REFUSED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + $result = (new State(State::GRANTED)) + ->canBeSetTo(State::VERIFIED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + + + $result = (new State(State::GRANTED)) + ->canBeSetTo(State::GRANTED); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + } +} diff --git a/test/Domain/DataStructure/RequestAccessFromApiKeyTest.php b/test/Domain/DataStructure/RequestAccessFromApiKeyTest.php new file mode 100644 index 0000000..1985461 --- /dev/null +++ b/test/Domain/DataStructure/RequestAccessFromApiKeyTest.php @@ -0,0 +1,38 @@ +fixture = FixtureRequestAccessFromApiKey::get(); + } + + public function testConstruct(): void + { + $entity = new RequestAccessFromApiKey( + FixtureApplication::get()->apiKey, + 300 + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(RequestAccessFromApiKey::class, $entity); + } +} diff --git a/test/Domain/DataStructure/RequestAccessFromOtpTest.php b/test/Domain/DataStructure/RequestAccessFromOtpTest.php new file mode 100644 index 0000000..07082f5 --- /dev/null +++ b/test/Domain/DataStructure/RequestAccessFromOtpTest.php @@ -0,0 +1,101 @@ +fixture = FixtureRequestAccessFromOtp::get(); + } + + public function testConstruct(): void + { + $entity = new RequestAccessFromOtp( + FixtureApplication::get(), + FixtureUser::get(), + 3, + 900 + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(RequestAccessFromOtp::class, $entity); + } + + public function testGetOtp(): void + { + $value = $this->fixture->getOtp(); + + $this->assertIsObject($value); + $this->assertInstanceOf(Otp::class, $value); + } + + public function testCheckOtp(): void + { + $result = $this->fixture->checkOtp( + $this->fixture->getOtp() + ); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + } + + public function testGetNumberOfRemainingAttempts(): void + { + $result = $this->fixture->getNumberOfRemainingAttempts(); + + $this->assertIsInt($result); + $this->assertEquals(3, $result); + } + + public function testCheckOtpNotAuthorized(): void + { + $this->expectException(NotAuthorized::class); + + $result = $this->fixture->checkOtp( + '000000' + ); + $this->assertEquals(false, $result); + + $result = $this->fixture->getNumberOfRemainingAttempts(); + $this->assertEquals(2, $result); + + $result = $this->fixture->checkOtp( + '000000' + ); + $this->assertEquals(false, $result); + + $result = $this->fixture->getNumberOfRemainingAttempts(); + $this->assertEquals(1, $result); + + $result = $this->fixture->checkOtp( + '000000' + ); + $this->assertEquals(false, $result); + + $result = $this->fixture->getNumberOfRemainingAttempts(); + $this->assertEquals(0, $result); + + $this->fixture->checkOtp( + '000000' + ); + } +} diff --git a/test/Domain/DataStructure/RequestAccessFromThirdPartyTest.php b/test/Domain/DataStructure/RequestAccessFromThirdPartyTest.php new file mode 100644 index 0000000..7a76d44 --- /dev/null +++ b/test/Domain/DataStructure/RequestAccessFromThirdPartyTest.php @@ -0,0 +1,46 @@ +fixture = FixtureRequestAccessFromThirdParty::get(); + } + + public function testConstruct(): void + { + $entity = new RequestAccessFromThirdParty( + FixtureApplication::get(), + new CallbackUrl('https://domain.ext/path'), + 900 + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(RequestAccessFromThirdParty::class, $entity); + } + + public function testGetCallbackUrl(): void + { + $value = $this->fixture->getCallbackUrl(); + + $this->assertIsObject($value); + $this->assertInstanceOf(CallbackUrl::class, $value); + } +} diff --git a/test/Domain/DataStructure/RequestAccessTest.php b/test/Domain/DataStructure/RequestAccessTest.php new file mode 100644 index 0000000..cd76425 --- /dev/null +++ b/test/Domain/DataStructure/RequestAccessTest.php @@ -0,0 +1,194 @@ +fixture = FixtureRequestAccessFromApiKey::get(); + } + + public function testGetId(): void + { + $value = $this->fixture->getId(); + + $this->assertIsObject($value); + $this->assertInstanceOf(Id::class, $value); + } + + public function testGetApplication(): void + { + $value = $this->fixture->getApplication(); + + $this->assertNull($value); + } + + public function testSetApplication(): void + { + $this->fixture->setApplication(FixtureApplication::get()); + + $value = $this->fixture->getApplication(); + + $this->assertIsObject($value); + $this->assertInstanceOf(Application::class, $value); + $this->assertEquals(FixtureApplication::get(), $value); + } + + public function testSetApplicationInvalid(): void + { + $this->expectException(NotAuthorized::class); + + $this->fixture->setApplication(FixtureApplication::get()); + $this->fixture->setApplication(FixtureApplication::get()); + } + + public function testGetUser(): void + { + $value = $this->fixture->getUser(); + + $this->assertNull($value); + } + + public function testSetUser(): void + { + $this->fixture->setUser(FixtureUser::get()); + + $value = $this->fixture->getUser(); + + $this->assertIsObject($value); + $this->assertInstanceOf(User::class, $value); + $this->assertEquals(FixtureUser::get(), $value); + } + + public function testSetUserInvalid(): void + { + $this->expectException(NotAuthorized::class); + + $this->fixture->setUser(FixtureUser::get()); + $this->fixture->setUser(FixtureUser::get()); + } + + public function testGetAuthMethod(): void + { + $value = $this->fixture->getAuthMethod(); + + $this->assertIsObject($value); + $this->assertInstanceOf(AuthMethod::class, $value); + } + + public function testGetState(): void + { + $value = $this->fixture->getState(); + + $this->assertIsObject($value); + $this->assertInstanceOf(State::class, $value); + } + + public function testCanBeSetStateTo(): void + { + $result = $this->fixture->canBeSetStateTo(new State(State::VERIFIED)); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + } + + public function testSetState(): void + { + $entity = $this->fixture->setState(new State(State::VERIFIED)); + + $this->assertIsObject($entity); + $this->assertInstanceOf(RequestAccessFromApiKey::class, $entity); + + $this->assertIsObject($entity->getState()); + $this->assertInstanceOf(State::class, $entity->getState()); + $this->assertEquals(new State(State::VERIFIED), $entity->getState()); + } + + public function testSetStateInvalid(): void + { + $this->expectException(NotAuthorized::class); + + $entity = $this->fixture->setState(new State(State::REQUESTED)); + } + + public function testGetLifetime(): void + { + $value = $this->fixture->getLifetime(); + + $this->assertIsInt($value); + } + + public function testGetExpiration(): void + { + $value = $this->fixture->getExpiration(); + + $this->assertIsInt($value); + } + + public function testTokenizeIdAndUntokenizeId(): void + { + $result = $this->fixture->tokenizeId(FixtureSslKey::get()); + + $this->assertIsObject($result); + $this->assertInstanceOf(Token::class, $result); + + $entity = $this->fixture->untokenizeId($result, FixtureSslKey::get()); + + $this->assertIsObject($entity); + $this->assertInstanceOf(Id::class, $entity); + } + + public function testTokenizeIdInvalid(): void + { + $this->expectException(NotCompliant::class); + + $this->fixture->tokenizeId(FixtureSslKey::getInvalid()); + } + + public function testUntokenizeIdInvalid(): void + { + $this->expectException(NotCompliant::class); + + $this->fixture->untokenizeId( + $this->fixture->tokenizeId(FixtureSslKey::get()), + FixtureSslKey::getInvalid() + ); + } + + public function testUntokenizeIdExpired(): void + { + $this->expectException(NotCompliant::class); + + $this->fixture->untokenizeId( + FixtureRequestAccessFromApiKey::getExpired()->tokenizeId(FixtureSslKey::get()), + FixtureSslKey::get() + ); + } +} diff --git a/test/Domain/DataStructure/SslKeyTest.php b/test/Domain/DataStructure/SslKeyTest.php new file mode 100644 index 0000000..d8dccbf --- /dev/null +++ b/test/Domain/DataStructure/SslKeyTest.php @@ -0,0 +1,74 @@ +fixture = FixtureSslKey::get(); + $this->fixtureInvalid = FixtureSslKey::getInvalid(); + } + + public function testGetPrivate(): void + { + $result = $this->fixture->getPrivate(); + + $this->assertIsString($result); + } + + public function testGetPublic(): void + { + $result = $this->fixture->getPublic(); + + $this->assertIsString($result); + } + + public function testEncrypt(): void + { + $result = $this->fixture->encrypt('Foo bar'); + + $this->assertIsString($result); + } + + public function testEncryptInvalid(): void + { + $this->expectException(NotCompliant::class); + + $result = $this->fixtureInvalid->encrypt('Foo bar'); + } + + public function testEncryptInvalidBis(): void + { + $this->expectException(NotCompliant::class); + + $result = $this->fixtureInvalid->encrypt(''); + } + + public function testDecrypt(): void + { + $result = $this->fixture->decrypt( + $this->fixture->encrypt('Foo bar') + ); + + $this->assertIsString($result); + } + + public function testDecryptInvalid(): void + { + $this->expectException(NotCompliant::class); + + $result = $this->fixtureInvalid->decrypt( + $this->fixture->encrypt('Foo bar') + ); + } +} diff --git a/test/Domain/DataStructure/UserTest.php b/test/Domain/DataStructure/UserTest.php new file mode 100644 index 0000000..a2978e4 --- /dev/null +++ b/test/Domain/DataStructure/UserTest.php @@ -0,0 +1,36 @@ +fixture = FixtureUser::get(); + } + + public function testConstruct(): void + { + $entity = new User( + 'john.doe@domain.ext', + 'John', + 'DOE' + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(User::class, $entity); + } +} diff --git a/test/Domain/Serialize/AccessTokenTest.php b/test/Domain/Serialize/AccessTokenTest.php new file mode 100644 index 0000000..b89952d --- /dev/null +++ b/test/Domain/Serialize/AccessTokenTest.php @@ -0,0 +1,31 @@ +fixture = FixtureAccessToken::get(); + } + + public function testSerialize(): void + { + $value = SerializeAccessToken::serialize( + $this->fixture + ); + + $this->assertIsArray($value); + $this->assertCount(2, $value); + $this->assertArrayHasKey('token', $value); + $this->assertArrayHasKey('expire', $value); + } +} diff --git a/test/Domain/Serialize/ApplicationTest.php b/test/Domain/Serialize/ApplicationTest.php new file mode 100644 index 0000000..943c39d --- /dev/null +++ b/test/Domain/Serialize/ApplicationTest.php @@ -0,0 +1,33 @@ +fixture = FixtureApplication::get(); + } + + public function testSerialize(): void + { + $value = SerializeApplication::serialize( + $this->fixture + ); + + $this->assertIsArray($value); + $this->assertCount(4, $value); + $this->assertArrayHasKey('id', $value); + $this->assertArrayHasKey('name', $value); + $this->assertArrayHasKey('logo', $value); + $this->assertArrayHasKey('api_key', $value); + } +} diff --git a/test/Domain/Serialize/UserTest.php b/test/Domain/Serialize/UserTest.php new file mode 100644 index 0000000..e7b07a6 --- /dev/null +++ b/test/Domain/Serialize/UserTest.php @@ -0,0 +1,33 @@ +fixture = FixtureUser::get(); + } + + public function testSerialize(): void + { + $value = SerializeUser::serialize( + $this->fixture + ); + + $this->assertIsArray($value); + $this->assertCount(4, $value); + $this->assertArrayHasKey('email_address', $value); + $this->assertArrayHasKey('lastname', $value); + $this->assertArrayHasKey('firstname', $value); + $this->assertArrayHasKey('role', $value); + } +} diff --git a/test/Domain/Service/AccessTokenTest.php b/test/Domain/Service/AccessTokenTest.php new file mode 100644 index 0000000..e83a091 --- /dev/null +++ b/test/Domain/Service/AccessTokenTest.php @@ -0,0 +1,74 @@ +service = (new FixtureServiceAccessToken())(); + $this->fixture = FixtureAccessToken::get(); + } + + public function testGetPublicKey(): void + { + $value = $this->service->getPublicKey(); + + $this->assertIsString($value); + } + + public function testCheck(): void + { + $value = $this->service->check( + (string)$this->fixture, + FixtureApplication::get() + ); + + $this->assertIsBool($value); + $this->assertEquals(true, $value); + } + + public function testGetFromToken(): void + { + $entity = $this->service->getFromToken( + FixtureRequestAccessFromOtp::getVerified() + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(AccessToken::class, $entity); + } + + public function testGetFromTokenInvalid(): void + { + $this->expectException(NotAuthorized::class); + + $entity = $this->service->getFromToken( + FixtureRequestAccessFromOtp::get() + ); + } + + public function testGetUserInfos(): void + { + $value = $this->service->getUserInfos( + (string)$this->fixture + ); + + $this->assertIsArray($value); + } +} diff --git a/test/Domain/Service/ApplicationTest.php b/test/Domain/Service/ApplicationTest.php new file mode 100644 index 0000000..dcdd64d --- /dev/null +++ b/test/Domain/Service/ApplicationTest.php @@ -0,0 +1,60 @@ +fixture = (new FixtureServiceApplication())(); + } + + public function testAdd(): void + { + $entity = $this->fixture->add( + 'Foo bar', + 'https://domain.ext/file.ext' + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(Application::class, $entity); + } + + public function testSet(): void + { + $this->fixture->set( + FixtureApplication::get() + ); + + $this->addToAssertionCount(1); + } + + public function testGet(): void + { + $entity = $this->fixture->get( + FixtureApplication::get()->id + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(Application::class, $entity); + } + + public function testGetFromApiKey(): void + { + $entity = $this->fixture->getFromApiKey( + FixtureApplication::get()->apiKey + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(Application::class, $entity); + } +} diff --git a/test/Domain/Service/RequestAccessFromApiKeyTest.php b/test/Domain/Service/RequestAccessFromApiKeyTest.php new file mode 100644 index 0000000..f103fc4 --- /dev/null +++ b/test/Domain/Service/RequestAccessFromApiKeyTest.php @@ -0,0 +1,43 @@ +service = (new FixtureServiceRequestAccessFromApiKey())(); + } + + public function testGetAccessToken(): void + { + $entity = $this->service->getAccessToken( + FixtureApplication::get()->apiKey + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(AccessToken::class, $entity); + } + + public function testGetAccessTokenInvalid(): void + { + $this->expectException(NotFound::class); + + $entity = $this->service->getAccessToken( + ApiKey::generate() + ); + + $this->assertNull($entity); + } +} diff --git a/test/Domain/Service/RequestAccessFromOtpTest.php b/test/Domain/Service/RequestAccessFromOtpTest.php new file mode 100644 index 0000000..d5e2876 --- /dev/null +++ b/test/Domain/Service/RequestAccessFromOtpTest.php @@ -0,0 +1,112 @@ +service = (new FixtureServiceRequestAccessFromOtp())(); + $this->fixture = $this->service->generate( + FixtureApplication::get(), + FixtureUser::get() + ); + $this->cache = new SimpleCache(realpath(__DIR__ . '/../../../test/storage/'), 'user-notification'); + } + + public function testGenerate(): void + { + $this->assertIsObject($this->fixture); + $this->assertInstanceOf(Token::class, $this->fixture); + } + + public function testGenerateInvalid(): void + { + $this->expectException(NotCompliant::class); + + $this->service->generate( + FixtureApplication::get(), + FixtureUser::get(), + 0 + ); + } + + public function testVerify(): void + { + $otp = $this->cache->get((string)$this->fixture); + + $result = $this->service->verify( + $this->fixture, + $otp + ); + + $this->assertIsBool($result); + $this->assertEquals(true, $result); + } + + public function testVerifyInvalid(): void + { + $result = $this->service->verify( + $this->fixture, + '000000' + ); + + $this->assertIsBool($result); + $this->assertEquals(false, $result); + } + + public function testGetNumberOfRemainingAttempts(): void + { + $result = $this->service->getNumberOfRemainingAttempts( + $this->fixture + ); + + $this->assertIsInt($result); + $this->assertEquals(3, $result); + } + + public function testGetAccessToken(): void + { + $otp = $this->cache->get((string)$this->fixture); + + $result = $this->service->verify( + $this->fixture, + $otp + ); + + $entity = $this->service->getAccessToken( + $this->fixture, + $otp + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(AccessToken::class, $entity); + } +} diff --git a/test/Domain/Service/RequestAccessFromThirdPartyTest.php b/test/Domain/Service/RequestAccessFromThirdPartyTest.php new file mode 100644 index 0000000..7bc1283 --- /dev/null +++ b/test/Domain/Service/RequestAccessFromThirdPartyTest.php @@ -0,0 +1,98 @@ +service = (new FixtureServiceRequestAccessFromThirdParty())(); + $this->fixture = $this->service->generate( + FixtureApplication::get(), + 'https://domain.ext/path' + ); + } + + public function testGenerate(): void + { + $this->assertIsObject($this->fixture); + $this->assertInstanceOf(Token::class, $this->fixture); + } + + public function testSetStatus(): void + { + $this->service->setStatus( + $this->fixture, + FixtureUser::get(), + true + ); + + $this->addToAssertionCount(1); + } + + public function testSetStatusToken(): void + { + $this->service->setStatus( + $this->fixture, + FixtureUser::get(), + true + ); + + $entity = $this->service->getAccessToken( + $this->fixture + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(AccessToken::class, $entity); + } + + public function testGetCallbackUrl(): void + { + $value = $this->service->getCallbackUrl( + $this->fixture + ); + + $this->assertIsObject($value); + $this->assertInstanceOf(CallbackUrl::class, $value); + } + + public function testGetAccessTokenInvalid(): void + { + $this->expectException(NotAuthorized::class); + + $this->service->setStatus( + $this->fixture, + FixtureUser::get(), + false + ); + + $entity = $this->service->getAccessToken( + $this->fixture + ); + } +} diff --git a/test/Domain/Service/RequestAccessTest.php b/test/Domain/Service/RequestAccessTest.php new file mode 100644 index 0000000..af54bba --- /dev/null +++ b/test/Domain/Service/RequestAccessTest.php @@ -0,0 +1,76 @@ +service = (new FixtureServiceRequestAccess())(); + $this->fixture = FixtureRequestAccessFromOtp::get(); + } + + public function testSet(): void + { + $this->service->set( + $this->fixture + ); + + $this->addToAssertionCount(1); + } + + public function testGet(): void + { + $this->service->set( + $this->fixture + ); + + $entity = $this->service->get( + $this->fixture->getId() + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(RequestAccessFromOtp::class, $entity); + } + + public function testGetToken(): void + { + $this->service->set( + $this->fixture + ); + + $value = $this->service->getToken( + $this->fixture + ); + + $this->assertIsObject($value); + $this->assertInstanceOf(Token::class, $value); + } + + public function testGetFromToken(): void + { + $this->service->set( + $this->fixture + ); + + $entity = $this->service->getFromToken( + $this->service->getToken( + $this->fixture + ) + ); + + $this->assertIsObject($entity); + $this->assertInstanceOf(RequestAccessFromOtp::class, $entity); + } +} diff --git a/test/storage/.gitkeep b/test/storage/.gitkeep new file mode 100644 index 0000000..e69de29