diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5f896a6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e61a75d..fac227c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +Release 40 (tag v1.3.0) + - Feat: Issue #50, Architecture change to map the local and cognito users with sub (SubjectId) + - Fix: Issue #86, SSO enabled the user is now created for both guards + - Fix: Code optimization + Release 39 (tag v1.2.5) - Fix: AWS JWT Token validation timeout - Fix: Non declared variable references diff --git a/README.md b/README.md index 933354b..17a4991 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,11 @@ AWS Cognito package using the AWS SDK for PHP [![GitHub Contributors](https://img.shields.io/github/contributors-anon/ellaisys/aws-cognito?style=flat&logo=github&logoColor=whitesmoke&label=Contributors)](CONTRIBUTING.md)  [![APM](https://img.shields.io/packagist/l/ellaisys/aws-cognito?style=flat-square&logo=github&logoColor=whitesmoke&label=License)](LICENSE.md) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ellaisys_aws-cognito&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ellaisys_aws-cognito) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ellaisys_aws-cognito&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ellaisys_aws-cognito) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=ellaisys_aws-cognito&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=ellaisys_aws-cognito) + + This package provides a simple way to use AWS Cognito authentication in Laravel for Web and API Auth Drivers. The idea of this package, and some of the code, is based on the package from Pod-Point which you can find here: [Pod-Point/laravel-cognito-auth](https://github.com/Pod-Point/laravel-cognito-auth), [black-bits/laravel-cognito-auth](https://github.com/black-bits/laravel-cognito-auth) and [tymondesigns/jwt-auth](https://github.com/tymondesigns/jwt-auth). @@ -20,12 +25,12 @@ The idea of this package, and some of the code, is based on the package from Pod We decided to use it and contribute it to the community as a package, that encourages standarised use and a RAD tool for authentication using AWS Cognito. ## Features -- [Registration and Confirmation E-Mail (Sign Up)](#registering-users) **Updated** (#9 feature added) +- [Registration and Confirmation E-Mail (Sign Up)](#registering-users) - Forced password change at first login (configurable) - [Login (Sign In)](#user-authentication) -- Token Validation for all Session and Token Guard Requests **New** +- Token Validation for all Session and Token Guard Requests - Remember Me Cookie -- Single Sign On +- Single Sign On **Updated** (Fix: Issue #86) - Forgot Password (Resend - configurable) - User Deletion - Edit User Attributes @@ -41,6 +46,7 @@ We decided to use it and contribute it to the community as a package, that encou - [Forced Logout (Sign Out) - Revoke the RefreshToken from AWS](#signout-remove-access-token) - [MFA Implementation for Session and Token Guards](./README_MFA.md) - [Password validation based on Cognito Configuration](#password-validation-based-of-cognito-configuration) +- [Mapping Cognito User using Subject UUID](#mapping-cognito-user-using-subject-uuid) **NEW** ## Compatability @@ -197,7 +203,6 @@ At the current state you need to have those 4 form fields defined in here. Those With our package and AWS Cognito we provide you a simple way to use Single Sign-Ons. For configuration options take a look at the config [cognito.php](/config/cognito.php). - When you want SSO enabled and a user tries to login into your application, the package checks if the user exists in your AWS Cognito pool. If the user exists, he will be created automatically in your database provided the `add_missing_local_user` is to `true`, and is logged in simultaneously. That's what we use the fields `sso_user_model` and `cognito_user_fields` for. In `sso_user_model` you define the class of your user model. In most cases this will simply be _App\Models\User_. @@ -626,6 +631,20 @@ This library fetches the password policy from the cognito pool configurations. T >[!IMPORTANT] >In case of special characters, we are supporting all except the pipe character **|** for now. +## Mapping Cognito User using Subject UUID + +The library maps the Cognito user subject UUID with the local repository. Everytime a new user is created in cognito, the sub UUID is mapped with the local user table with an user specified column name. + +The column in the local BD is identified with the config parameter `user_subject_uuid` with the default value set to `sub`. + +However, to customize the column name in the local DB user table, you may do that with below setting fields to your `.env` file + +```php + + AWS_COGNITO_USER_SUBJECT_UUID="sub" + +``` + We are working on making sure that pipe character is handled soon. ## Changelog diff --git a/composer.json b/composer.json index 6beb71c..e6a9000 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "ellaisys/aws-cognito", "description": "AWS Cognito package that allows Auth and other related features using the AWS SDK for PHP", - "keywords": ["php", "laravel", "aws", "cognito", "auth", "authentication", "oauth", "user pool", "ellaisys"], + "keywords": ["php", "laravel", "aws", "cognito", "auth", "authentication", "oauth", "user pool", "ellaisys", "mfa", "multi-factor authentication", "2fa", "two-factor authentication", "password", "reset", "forgot", "change", "update", "email", "phone", "sms", "email verification", "phone verification", "sms verification", "email confirmation", "phone confirmation", "sms confirmation", "email code", "phone code", "sms code", "email token", "phone token", "sms token", "email password", "phone password", "sms password", "email recovery", "phone recovery", "sms recovery", "email reset", "phone reset", "sms reset", "email forgot", "phone forgot", "sms forgot", "email change", "phone change", "sms change", "email update", "phone update", "sms update", "email verify", "phone verify", "sms verify", "email confirm", "phone confirm", "sms confirm", "email code verification", "phone code verification", "sms code verification", "email token verification", "phone token verification", "sms token verification", "email password reset", "phone password reset", "sms password reset", "email recovery password", "phone recovery password", "sms recovery password", "email reset password", "phone reset password", "sms reset password", "email forgot password", "phone forgot password", "sms forgot password", "email change password", "phone change password", "sms change password", "email update password", "phone update password", "sms update password", "email verify code", "phone verify code", "sms verify code", "email confirm code", "phone confirm code", "sms confirm code", "email code verify", "phone code verify", "sms code verify", "email token verify", "phone token verify", "sms token verify", "email password reset code", "phone password reset code", "sms password reset code", "email recovery password code", "phone recovery password code", "sms recovery password code", "email reset password code", "phone reset password code", "sms reset password code", "email forgot password code", "phone forgot password code", "sms forgot password code", "email change password code", "phone change password code", "sms change password code", "email update password code", "phone update password code", "sms update password code", "email verify code verification", "phone verify code verification", "sms verify code verification", "email confirm code verification", "phone confirm code verification", "sms confirm code verification", "email code verify verification", "phone code verify verification", "sms"], "type": "library", "license": "MIT", "homepage": "https://ellaisys.github.io/aws-cognito/", diff --git a/config/cognito.php b/config/cognito.php index 153ed44..96fee6d 100644 --- a/config/cognito.php +++ b/config/cognito.php @@ -55,6 +55,12 @@ | This option controls the default cognito fields that shall be needed to be | updated. The array value is a mapping with DB model or Request data. | + | DO NOT change the parameters on the left side of the array. They map to + | the AWS Cognito User Pool fields. + | + | The right side of the array is the DB model field, and you can set the + | value to null if you do not want to update the field. + | */ 'cognito_user_fields' => [ 'name' => 'name', @@ -64,12 +70,26 @@ 'nickname' => null, 'preferred_username' => null, 'email' => 'email', //Do Not set this parameter to null - 'phone_number' => 'phone', + 'phone_number' => null, 'gender' => null, 'birthdate' => null, 'locale' => null ], + + /* + |-------------------------------------------------------------------------- + | Cognito Subject UUID + |-------------------------------------------------------------------------- + | + | This option controls the default cognito subject UUID that shall be needed + | to be updated based on your local DB schema. This value is the attribute + | in the local DB Model that maps with Cognito user subject UUID. + | + */ + 'user_subject_uuid' => env('AWS_COGNITO_USER_SUBJECT_UUID', 'sub'), + + /* |-------------------------------------------------------------------------- | Cognito New User @@ -104,7 +124,7 @@ |-------------------------------------------------------------------------- | | This option controls the cognito MFA configuration for the assigned user. - | + | | | MFA_NONE, MFA_ENABLED | @@ -137,7 +157,7 @@ | */ 'add_missing_local_user' => env('AWS_COGNITO_ADD_LOCAL_USER', false), - 'delete_user' => env('AWS_COGNITO_DELETE_USER', false), + 'delete_user' => env('AWS_COGNITO_DELETE_USER', false), // Package configurations 'sso_user_model' => env('AWS_COGNITO_USER_MODEL', 'App\Models\User'), diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..7e60662 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,13 @@ +sonar.projectKey=ellaisys_aws-cognito +sonar.organization=ellaisys + +# This is the name and version displayed in the SonarCloud UI. +sonar.projectName=aws-cognito +sonar.projectVersion=1.3.0 + + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +#sonar.sources=. + +# Encoding of the source code. Default is default system encoding +sonar.sourceEncoding=UTF-8 diff --git a/src/Auth/AuthenticatesUsers.php b/src/Auth/AuthenticatesUsers.php index 6bb72e2..40cd7be 100644 --- a/src/Auth/AuthenticatesUsers.php +++ b/src/Auth/AuthenticatesUsers.php @@ -53,7 +53,7 @@ protected function getAdminListGroupsForUser(string $username) foreach ($groups as $key => &$value) { unset($value['UserPoolId']); unset($value['RoleArn']); - } //Loop ends + } //Loop ends } //End if } //End if } catch(Exception $e) { @@ -75,9 +75,16 @@ protected function getAdminListGroupsForUser(string $username) * * @return mixed */ - protected function attemptLogin(Collection $request, string $guard='web', string $paramUsername='email', string $paramPassword='password', bool $isJsonResponse=false) + protected function attemptLogin(Request|Collection $request, string $guard='web', string $paramUsername='email', string $paramPassword='password', bool $isJsonResponse=false) { try { + $returnValue = null; + + //Convert request to collection + if ($request instanceof Request) { + $request = collect($request->all()); + } //End if + //Get the password policy $passwordPolicy = app()->make(AwsCognitoUserPool::class)->getPasswordPolicy(true); @@ -88,44 +95,34 @@ protected function attemptLogin(Collection $request, string $guard='web', string 'regex' => 'Must contain atleast ' . $passwordPolicy['message'] ]); if ($validator->fails()) { - Log::info($validator->errors()); + Log::error($validator->errors()); throw new ValidationException($validator); } //End if - //Get the configuration fields - $userFields = config('cognito.cognito_user_fields'); - - //Get key fields - $keyUsername = $userFields['email']; - $keyPassword = 'password'; - $rememberMe = $request->has('remember')?$request['remember']:false; - - //Generate credentials array - $credentials = [ - $keyUsername => $request[$paramUsername], - $keyPassword => $request[$paramPassword] - ]; - //Authenticate User - $claim = Auth::guard($guard)->attempt($credentials, $rememberMe); + $returnValue = Auth::guard($guard)->attempt($request->toArray(), false, $paramUsername, $paramPassword); + } catch (NoLocalUserException | CognitoIdentityProviderException | Exception $e) { + $exceptionClass = basename(str_replace('\\', DIRECTORY_SEPARATOR, get_class($e))); + $exceptionCode = $e->getCode(); + $exceptionMessage = $e->getMessage().':(code:'.$exceptionCode.', line:'.$e->getLine().')'; + if ($e instanceof CognitoIdentityProviderException) { + $exceptionCode = $e->getAwsErrorCode(); + $exceptionMessage = $e->getAwsErrorMessage().':'.$exceptionCode; + } //End if + Log::error('AuthenticatesUsers:attemptLogin:'.$exceptionClass.':'.$exceptionMessage); - } catch (NoLocalUserException $e) { - Log::error('AuthenticatesUsers:attemptLogin:NoLocalUserException'); - $user = $this->createLocalUser($credentials, $keyPassword); - if ($user) { - return $user; + if ($e instanceof ValidationException) { + throw $e; } //End if - return $this->sendFailedLoginResponse($request, $e, $isJsonResponse, $paramUsername); - } catch (CognitoIdentityProviderException $e) { - Log::error('AuthenticatesUsers:attemptLogin:CognitoIdentityProviderException'); - return $this->sendFailedCognitoResponse($e, $isJsonResponse, $paramUsername); - } catch (Exception $e) { - Log::error('AuthenticatesUsers:attemptLogin:Exception'); - return $this->sendFailedLoginResponse($request, $e, $isJsonResponse, $paramUsername); + if ($e instanceof CognitoIdentityProviderException) { + $this->sendFailedCognitoResponse($e, $isJsonResponse, $paramUsername); + } + + $returnValue = $this->sendFailedLoginResponse($request, $e, $isJsonResponse, $paramUsername); } //Try-catch ends - return $claim; + return $returnValue; } //Function ends @@ -156,7 +153,6 @@ protected function attemptLoginMFA($request, string $guard='web', bool $isJsonRe $challenge = $request->only(['challenge_name', 'session', 'mfa_code'])->toArray(); //Fetch user details - $user = null; switch ($guard) { case 'web': //Web if (request()->session()->has($challenge['session'])) { @@ -164,7 +160,6 @@ protected function attemptLoginMFA($request, string $guard='web', bool $isJsonRe $sessionToken = request()->session()->get($challenge['session']); $username = $sessionToken['username']; $challenge['username'] = $username; - $user = unserialize($sessionToken['user']); } else{ throw new HttpException(400, 'ERROR_AWS_COGNITO_SESSION_MFA_CODE'); } //End if @@ -174,29 +169,20 @@ protected function attemptLoginMFA($request, string $guard='web', bool $isJsonRe $challengeData = Auth::guard($guard)->getChallengeData($challenge['session']); $username = $challengeData['username']; $challenge['username'] = $username; - $user = unserialize($challengeData['user']); break; default: - $user = null; break; } //End switch //Authenticate User - $claim = Auth::guard($guard)->attemptMFA($challenge, $user); + $claim = Auth::guard($guard)->attemptMFA($challenge); } catch (NoLocalUserException $e) { Log::error('AuthenticatesUsers:attemptLoginMFA:NoLocalUserException'); - - $response = $this->createLocalUser($user->toArray()); - if ($response) { - return $response; - } //End if - return $this->sendFailedLoginResponse($request, $e, $isJsonResponse, $paramUsername); } catch (CognitoIdentityProviderException $e) { Log::error('AuthenticatesUsers:attemptLoginMFA:CognitoIdentityProviderException'); return $this->sendFailedLoginResponse($request, $e, $isJsonResponse, $paramName); - } catch (Exception $e) { Log::error('AuthenticatesUsers:attemptLoginMFA:Exception'); Log::error($e); @@ -216,32 +202,6 @@ protected function attemptLoginMFA($request, string $guard='web', bool $isJsonRe } //Function ends - /** - * Create a local user if one does not exist. - * - * @param array $credentials - * @return mixed - */ - protected function createLocalUser(array $dataUser, string $keyPassword='password') - { - $user = null; - if (config('cognito.add_missing_local_user')) { - //Get user model from configuration - $userModel = config('cognito.sso_user_model'); - - //Remove password from credentials if exists - if (array_key_exists($keyPassword, $dataUser)) { - unset($dataUser[$keyPassword]); - } //End if - - //Create user - $user = $userModel::create($dataUser); - } //End if - - return $user; - } //Function ends - - /** * Handle Failed Cognito Exception * @@ -263,33 +223,34 @@ private function sendFailedCognitoResponse(CognitoIdentityProviderException $exc */ private function sendFailedLoginResponse($request, $exception=null, bool $isJsonResponse=false, string $paramName='email') { - $errorCode = 'cognito.validation.auth.failed'; + $errorCode = 400; + $errorMessageCode = 'cognito.validation.auth.failed'; $message = 'FailedLoginResponse'; if (!empty($exception)) { if ($exception instanceof CognitoIdentityProviderException) { - $errorCode = $exception->getAwsErrorCode(); + $errorMessageCode = $exception->getAwsErrorCode(); $message = $exception->getAwsErrorMessage(); } elseif ($exception instanceof ValidationException) { throw $exception; } else { + $errorCode = $exception->getStatusCode(); $message = $exception->getMessage(); } //End if } //End if if ($isJsonResponse) { return response()->json([ - 'error' => $errorCode, + 'error' => $errorMessageCode, 'message' => $message - ], 400); + ], $errorCode); } else { return redirect() ->back() ->withErrors([ + 'error' => $errorMessageCode, $paramName => $message, ]); } //End if - - throw new HttpException(400, $message); } //Function ends diff --git a/src/AwsCognitoClaim.php b/src/AwsCognitoClaim.php index 4e093d3..615371f 100644 --- a/src/AwsCognitoClaim.php +++ b/src/AwsCognitoClaim.php @@ -52,6 +52,12 @@ class AwsCognitoClaim public $sub; + /** + * @var object + */ + public $tokenDecode; + + /** * Create a new JSON Web Token. * @@ -59,8 +65,7 @@ class AwsCognitoClaim * * @return void */ - public function __construct(AwsResult $result, Authenticatable $user=null, string $username) - { + public function __construct(AwsResult $result, Authenticatable $user=null) { try { $authResult = $result['AuthenticationResult']; if (!is_array($authResult)) { @@ -72,9 +77,13 @@ public function __construct(AwsResult $result, Authenticatable $user=null, strin $this->token = (string) (new AwsCognitoTokenValidator)->check($token); $this->data = $authResult; - $this->username = $username; + + //Decode the token + $decodedToken = (array) (new AwsCognitoTokenValidator)->decode($token); + $this->username = $decodedToken['username']; $this->user = $user; - $this->sub = $user['id']; + $this->sub = $decodedToken['sub']; + $this->tokenDecode = $decodedToken; } catch(Exception $e) { throw $e; @@ -87,7 +96,7 @@ public function __construct(AwsResult $result, Authenticatable $user=null, strin * * @return string */ - public function getToken() + public function getToken(): string { return $this->token; } //Function ends @@ -115,6 +124,27 @@ public function getUser() } //Function ends + /** + * Set the User. + * + */ + public function setUser(Authenticatable $user) + { + $this->user = $user; + } //Function ends + + + /** + * Get the Username. + * + * @return \string + */ + public function getUsername(): string + { + return $this->username; + } //Function ends + + /** * Get the Sub Data. * @@ -126,6 +156,17 @@ public function getSub() } //Function ends + /** + * Get the Decoded Token Data. + * + * @return mixed + */ + public function getDecodeToken() + { + return $this->tokenDecode; + } //Function ends + + /** * Get the token when casting to string. * diff --git a/src/Exceptions/AwsCognitoException.php b/src/Exceptions/AwsCognitoException.php index 0c0aad7..0bf60d9 100644 --- a/src/Exceptions/AwsCognitoException.php +++ b/src/Exceptions/AwsCognitoException.php @@ -7,29 +7,21 @@ use Symfony\Component\HttpKernel\Exception\HttpException; -class AwsCognitoException extends Exception +class AwsCognitoException extends HttpException { /** - * Report the exception. + * Create a new exception instance. * - * @return void - */ - public function report($message="AWS Cognito Error", $code=null, Throwable $previous=null) - { - throw new HttpException(400, $message, $previous, [], $code); - } - - - /** - * Render the exception into an HTTP response. + * @param string $message + * @param int $code + * @param \Throwable $previous + * @param array $headers * - * @param \Illuminate\Http\Request $request - * @param \Throwable $exception - * @return \Illuminate\Http\Response + * @return void */ - public function render($request, Throwable $exception) + public function __construct($message="AWS Cognito Error", $code=null, Throwable $previous=null, array $headers=[]) { - return parent::render($request, $exception); + parent::__construct(400, $message, $previous, $headers, $code); } -} //Class ends \ No newline at end of file +} //Class ends diff --git a/src/Exceptions/InvalidUserException.php b/src/Exceptions/InvalidUserException.php index 35e1838..cd3e672 100644 --- a/src/Exceptions/InvalidUserException.php +++ b/src/Exceptions/InvalidUserException.php @@ -10,7 +10,7 @@ class InvalidUserException extends HttpException { - public function __construct(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(string $message = 'Invalid Cognito User', \Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(400, $message, $previous, $headers, $code); } diff --git a/src/Exceptions/NoLocalUserException.php b/src/Exceptions/NoLocalUserException.php index 6db2b81..7a88106 100644 --- a/src/Exceptions/NoLocalUserException.php +++ b/src/Exceptions/NoLocalUserException.php @@ -5,31 +5,22 @@ use Throwable; use Exception; -use Illuminate\Database\Eloquent\ModelNotFoundException; +use Symfony\Component\HttpKernel\Exception\HttpException; -class NoLocalUserException extends Exception +class NoLocalUserException extends HttpException { /** - * Report the exception. + * Create a new exception instance. * - * @return void - */ - public function report() - { - throw new ModelNotFoundException(); - } - - - /** - * Render the exception into an HTTP response. + * @param string $message + * @param \Throwable $previous + * @param int $code + * @param array $headers * - * @param \Illuminate\Http\Request $request - * @param \Throwable $exception - * @return \Illuminate\Http\Response + * @return void */ - public function render($request, Throwable $exception) + public function __construct(string $message = 'User does not exist locally.', Throwable $previous = null, int $code = 0, array $headers = []) { - return parent::render($request, $exception); + parent::__construct(401, $message, $previous, $headers, $code); } - -} //Class ends \ No newline at end of file +} //Class ends diff --git a/src/Guards/CognitoSessionGuard.php b/src/Guards/CognitoSessionGuard.php index 3912bd3..e4536bf 100644 --- a/src/Guards/CognitoSessionGuard.php +++ b/src/Guards/CognitoSessionGuard.php @@ -13,6 +13,7 @@ use Aws\Result as AwsResult; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use Illuminate\Auth\SessionGuard; @@ -47,8 +48,8 @@ class CognitoSessionGuard extends SessionGuard implements StatefulGuard /** * Username key - * - * @var \string + * + * @var \string */ protected $keyUsername; @@ -65,6 +66,14 @@ class CognitoSessionGuard extends SessionGuard implements StatefulGuard * @var \Ellaisys\Cognito\AwsCognito */ protected $cognito; + + + /** + * The AwsCognito Claim token + * + * @var \Ellaisys\Cognito\AwsCognitoClaim|null + */ + protected $claim; /** @@ -80,19 +89,19 @@ class CognitoSessionGuard extends SessionGuard implements StatefulGuard /** - * @var Challenge Data based on + * @var Challenge Data based on the challenge */ protected $challengeData; /** * CognitoSessionGuard constructor. - * + * * @param string $name * @param AwsCognitoClient $client * @param UserProvider $provider * @param Session $session - * @param null|Request $request + * @param Request $request */ public function __construct( @@ -101,7 +110,7 @@ public function __construct( AwsCognitoClient $client, UserProvider $provider, Session $session, - ?Request $request = null, + Request $request, string $keyUsername = 'email' ) { $this->cognito = $cognito; @@ -113,60 +122,6 @@ public function __construct( } - /** - * @param mixed $user - * @param array $credentials - * @return bool - * @throws InvalidUserModelException - */ - protected function hasValidCredentials($user, $credentials) - { - $result = $this->client->authenticate($credentials['email'], $credentials['password']); - - if (!empty($result) && $result instanceof AwsResult) { - //Set value into class param - $this->awsResult = $result; - - //Check in case of any challenge - if (isset($result['ChallengeName'])) { - - //Set challenge into class param - $this->challengeName = $result['ChallengeName']; - switch ($result['ChallengeName']) { - case 'SOFTWARE_TOKEN_MFA': - $this->challengeData = [ - 'status' => $result['ChallengeName'], - 'session_token' => $result['Session'], - 'username' => $credentials[$this->keyUsername], - 'user' => serialize($user) - ]; - break; - - case 'SMS_MFA': - $this->challengeData = [ - 'status' => $result['ChallengeName'], - 'session_token' => $result['Session'], - 'challenge_params' => $result['ChallengeParameters'], - 'username' => $credentials[$this->keyUsername], - 'user' => serialize($user) - ]; - break; - - default: - if (in_array($result['ChallengeName'], config('cognito.forced_challenge_names'))) { - $this->challengeName = $result['ChallengeName']; - } //End if - break; - } //End switch - } //End if - - return ($user instanceof Authenticatable)?true:false; - } //End if - - return false; - } //Function ends - - /** * Attempt to authenticate an existing user using the credentials * using Cognito @@ -176,102 +131,163 @@ protected function hasValidCredentials($user, $credentials) * @throws * @return bool */ - public function attempt(array $credentials = [], $remember = false) + public function attempt(array $credentials = [], $remember = false, string $paramUsername='email', string $paramPassword='password') { try { + $returnValue = false; + $user = null; + + //convert to collection + $request = collect($credentials); + + //Build the payload + $payloadCognito = $this->buildCognitoPayload($request, $paramUsername, $paramPassword); + //Fire event for authenticating - $this->fireAttemptEvent($credentials, $remember); + $this->fireAttemptEvent($request->toArray(), $remember); - //Get user from presisting store - $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); + //Check if the payload has valid AWS credentials + $responseCognito = collect($this->hasValidAWSCredentials($payloadCognito)); + if ($responseCognito && (!empty($this->claim))) { + //Process the claim + if ($user = $this->processAWSClaim()) { + //Login user into the session + $this->login($user, $remember); - //Check if the user exists in local data store - if (empty($user) && !($user instanceof Authenticatable)) { - throw new NoLocalUserException(); - } //End if + //Fire successful attempt + $this->fireLoginEvent($user, true); - //Authenticate with cognito - if ($this->hasValidCredentials($user, $credentials)) { - if (!empty($this->challengeName)) { - switch ($this->challengeName) { - case 'SOFTWARE_TOKEN_MFA': - case 'SMS_MFA': - //Get Session and store details - $session = $this->getSession(); - $session->invalidate(); - $session->put($this->challengeData['session_token'], json_decode(json_encode($this->challengeData), true)); - - return redirect(route(config('cognito.force_mfa_code_route_name'), [ - 'session_token' => $this->challengeData['session_token'], - 'status' => $this->challengeData['status'], - ])) - ->with('success', true) - ->with('force', true) - ->with('messaage', $this->challengeName); - break; - - case AwsCognitoClient::NEW_PASSWORD_CHALLENGE: - case AwsCognitoClient::RESET_REQUIRED_PASSWORD: - $this->login($user, $remember); - - if (config('cognito.force_password_change_web', false)) { - return redirect(route(config('cognito.force_redirect_route_name'))) - ->with('success', true) - ->with('force', true) - ->with('messaage', $this->challengeName); - } //End if - break; - - default: - if (in_array($this->challengeName, config('cognito.forced_challenge_names'))) { - $this->challengeName = $result['ChallengeName']; - } //End if - break; - } //End switch - } else { - //Create Claim for confirmed users and store into session - if (!empty($this->awsResult)) { - //Create claim token - $claim = new AwsCognitoClaim($this->awsResult, $user, $credentials[$this->keyUsername]); - - //Get Session and store details - $session = $this->getSession(); - $session->invalidate(); - $session->put('claim', json_decode(json_encode($claim), true)); - - $this->login($user, $remember); - - //Fire successful attempt - $this->fireValidatedEvent($user); - $this->fireAuthenticatedEvent($user); - } else { - throw new HttpException(400, 'ERROR_AWS_COGNITO'); - } //End if + $returnValue = true; } //End if - - return true; + } elseif ($responseCognito && $this->challengeName) { + //Handle the challenge + $returnValue = $this->handleAWSChallenge(); + } else { + throw new AwsCognitoException('ERROR_AWS_COGNITO'); } //End if + } catch (CognitoIdentityProviderException $e) { + Log::error('CognitoSessionGuard:attempt:CognitoIdentityProviderException:'.$e->getAwsErrorCode()); - //Fire failed attempt - $this->fireFailedEvent($user, $credentials); + //Handle the exception + $returnValue = $this->handleCognitoException($e); + } catch (NoLocalUserException | AwsCognitoException | Exception $e) { + $exceptionClass = basename(str_replace('\\', DIRECTORY_SEPARATOR, get_class($e))); + $exceptionCode = $e->getCode(); + $exceptionMessage = $e->getMessage().':(code:'.$exceptionCode.', line:'.$e->getLine().')'; + if ($e instanceof CognitoIdentityProviderException) { + $exceptionCode = $e->getAwsErrorCode(); + $exceptionMessage = $e->getAwsErrorMessage().':'.$exceptionCode; + } //End if + Log::error('CognitoSessionGuard:attempt:'.$exceptionClass.':'.$exceptionMessage); - return false; - } catch (NoLocalUserException $e) { - Log::error('CognitoSessionGuard:attempt:NoLocalUserException:'.$e->getMessage()); + //Find SQL Exception + if (strpos($e->getMessage(), 'SQLSTATE') !== false) { + throw new DBConnectionException(); + } //End if //Fire failed attempt - $this->fireFailedEvent($user, $credentials); + if (!$returnValue) { + $this->fireFailedEvent($user, $request->toArray()); + } //End if throw $e; - } catch (CognitoIdentityProviderException $e) { - Log::error('CognitoSessionGuard:attempt:CognitoIdentityProviderException:'.$e->getAwsErrorCode()); + } //Try-catch ends + + return $returnValue; + } //Function ends - //Fire failed attempt - $this->fireFailedEvent($user, $credentials); + /** + * Process the AWS Claim and Authenticate the user with the local database + * + * @return Authenticatable + */ + private function processAWSClaim(): Authenticatable { + $credentials = collect([ + config('cognito.user_subject_uuid') => $this->claim->getSub() + ]); + + //Check if the user exists + $this->lastAttempted = $user = $this->hasValidLocalCredentials($credentials); + if (!empty($user) && ($user instanceof Authenticatable)) { + + //Save the user data into the claim + $this->claim->setUser($user); + + //Get Session and store details + $session = $this->getSession(); + $session->invalidate(); + $session->put('claim', json_decode(json_encode($this->claim), true)); + + //Fire successful attempt + $this->fireValidatedEvent($user); + $this->fireAuthenticatedEvent($user); + + return $user; + } else { + throw new NoLocalUserException(); + } //End if + } //Function ends + + + /** + * Handle the AWS Challenge + * + * @return mixed + */ + private function handleAWSChallenge() { + $returnValue = null; + + switch ($this->challengeName) { + case 'SOFTWARE_TOKEN_MFA': + case 'SMS_MFA': + //Get Session and store details + $session = $this->getSession(); + $session->invalidate(); + $session->put($this->challengeData['session_token'], json_decode(json_encode($this->challengeData), true)); + + $returnValue = redirect(route(config('cognito.force_mfa_code_route_name'), [ + 'session_token' => $this->challengeData['session_token'], + 'status' => $this->challengeData['status'], + ])) + ->with('success', true) + ->with('force', true) + ->with('messaage', $this->challengeName); + break; + + case AwsCognitoClient::NEW_PASSWORD_CHALLENGE: + case AwsCognitoClient::RESET_REQUIRED_PASSWORD: + $this->login($user, $remember); + + if (config('cognito.force_password_change_web', false)) { + $returnValue = redirect(route(config('cognito.force_redirect_route_name'))) + ->with('success', true) + ->with('force', true) + ->with('messaage', $this->challengeName); + } //End if + break; + + default: + if (in_array($this->challengeName, config('cognito.forced_challenge_names'))) { + $this->challengeName = $result['ChallengeName']; + } //End if + break; + } //End switch + + return $returnValue; + } //Funtion ends + + + /** + * Handle the AWS Cognito Exception + * + * @param CognitoIdentityProviderException $e + * @return mixed + */ + private function handleCognitoException(CognitoIdentityProviderException $e) { + if ($e instanceof CognitoIdentityProviderException) { //Set proper route if (!empty($e->getAwsErrorCode())) { - // sonarignore:start switch ($e->getAwsErrorCode()) { case 'PasswordResetRequiredException': return redirect(route('cognito.form.reset.password.code')) @@ -286,43 +302,23 @@ public function attempt(array $credentials = [], $remember = false) throw $e; break; } //End switch - // sonarignore:end } //End if return $e->getAwsErrorCode(); - } catch (AwsCognitoException $e) { - Log::error('CognitoSessionGuard:attempt:AwsCognitoException:'.$e->getMessage()); - - //Fire failed attempt - $this->fireFailedEvent($user, $credentials); - - throw $e; - } catch (Exception $e) { - Log::error('CognitoSessionGuard:attempt:Exception:'.$e->getMessage()); - - //Fire failed attempt - if (!empty($user)) { - $this->fireFailedEvent($user, $credentials); - } //End if - - //Find SQL Exception - if (strpos($e->getMessage(), 'SQLSTATE') !== false) { - throw new DBConnectionException(); - } //End if - - throw $e; - } //Try-catch ends + } else { + return $e->getAwsErrorCode(); + } //End if } //Function ends /** - * Logout the user, thus invalidating the token. + * Logout the user, thus invalidating the session. * * @param bool $forceForever * * @return void */ - public function logout($forceForever = false) + public function logout(bool $forceForever = false) { $this->invalidate($forceForever); $this->user = null; @@ -338,6 +334,9 @@ public function logout($forceForever = false) */ public function invalidate($forceForever = false) { + //Return Value + $returnValue = null; + try { //Get authentication token from session $session = $this->getSession(); @@ -368,66 +367,55 @@ public function invalidate($forceForever = false) } //End if //Remove the token from application storage - return $session->invalidate(); + $returnValue = $session->invalidate(); } else { //Remove the token from application storage - return $session->invalidate(); + $returnValue = $session->invalidate(); } //End if } else { //Remove the token from application storage - return $session->invalidate(); + $returnValue = $session->invalidate(); } //End if } catch (Exception $e) { if ($forceForever) { return $session->invalidate(); } throw $e; } //try-catch ends + + return $returnValue; } //Function ends /** * Attempt MFA based Authentication */ - public function attemptMFA(array $challenge, Authenticatable $user, bool $remember=false) { + public function attemptMFA(array $challenge=[], bool $remember=false) { + $returnValue = false; try { - $claim = null; - - $response = $this->attemptBaseMFA($challenge, $user, $remember); - //Result of type AWS Result - if (!empty($response)) { - - //Handle the response as Aws Cognito Claim - if ($response instanceof AwsCognitoClaim) { - $claim = $response; - - //Get Session and store details - $session = $this->getSession(); - $session->forget($challenge['session']); - $session->put('claim', json_decode(json_encode($claim), true)); - + //Login with MFA Challenge + $responseCognito = $this->attemptBaseMFA($challenge, $remember); + if ($responseCognito && (!empty($this->claim))) { + //Process the claim + if ($user = $this->processAWSClaim()) { //Login user into the session $this->login($user, $remember); //Fire successful attempt - $this->fireValidatedEvent($user); - $this->fireAuthenticatedEvent($user); - - return true; - } //End if - - //Handle if the object is a Aws Cognito Result - if ($response instanceof AwsResult) { - //Check in case of any challenge - // if (isset($response['ChallengeName'])) { + $this->fireLoginEvent($user, true); - // } else { - - // } //End if + $returnValue = true; } //End if + } elseif ($responseCognito && $this->challengeName) { + //Handle the challenge + $returnValue = $this->handleAWSChallenge(); + } else { + throw new HttpException(400, 'ERROR_AWS_COGNITO'); } //End if } catch(Exception $e) { throw $e; } //Try-catch ends + + return $returnValue; } //Function ends } //Class ends diff --git a/src/Guards/CognitoTokenGuard.php b/src/Guards/CognitoTokenGuard.php index 96ead76..afa13b9 100644 --- a/src/Guards/CognitoTokenGuard.php +++ b/src/Guards/CognitoTokenGuard.php @@ -14,6 +14,7 @@ use Aws\Result as AwsResult; use Illuminate\Http\Request; use Illuminate\Auth\TokenGuard; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Auth\Authenticatable; @@ -27,6 +28,7 @@ use Exception; use Ellaisys\Cognito\Exceptions\NoLocalUserException; +use Ellaisys\Cognito\Exceptions\InvalidUserException; use Ellaisys\Cognito\Exceptions\InvalidUserModelException; use Ellaisys\Cognito\Exceptions\AwsCognitoException; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -39,8 +41,8 @@ class CognitoTokenGuard extends TokenGuard /** * Username key - * - * @var \string + * + * @var \string */ protected $keyUsername; @@ -61,15 +63,33 @@ class CognitoTokenGuard extends TokenGuard /** * The AwsCognito Claim token - * + * * @var \Ellaisys\Cognito\AwsCognitoClaim|null */ protected $claim; + + + /** + * @var Authentication Challenge + */ + protected $challengeName; + + + /** + * @var AwsResult + */ + protected $awsResult; + + + /** + * @var Challenge Data based on the challenge + */ + protected $challengeData; /** * CognitoTokenGuard constructor. - * + * * @param $callback * @param AwsCognitoClient $client * @param Request $request @@ -77,10 +97,10 @@ class CognitoTokenGuard extends TokenGuard */ public function __construct( AwsCognito $cognito, - AwsCognitoClient $client, - Request $request, + AwsCognitoClient $client, + Request $request, UserProvider $provider = null, - string $keyUsername + string $keyUsername = null ) { $this->cognito = $cognito; $this->client = $client; @@ -90,84 +110,6 @@ public function __construct( } - /** - * @param mixed $user - * @param array $credentials - * @return bool - * @throws InvalidUserModelException - */ - protected function hasValidCredentials($user, array $credentials, bool $remember = false) - { - /** @var Result $response */ - $result = $this->client->authenticate($credentials[$this->keyUsername], $credentials['password']); - - //Result of type AWS Result - if (!empty($result) && $result instanceof AwsResult) { - - //Check in case of any challenge - if (isset($result['ChallengeName'])) { - switch ($result['ChallengeName']) { - case 'SOFTWARE_TOKEN_MFA': - $this->claim = [ - 'status' => $result['ChallengeName'], - 'session' => $result['Session'], - 'username' => $credentials[$this->keyUsername], - 'user' => serialize($user) - ]; - break; - - case 'SMS_MFA': - $this->claim = [ - 'status' => $result['ChallengeName'], - 'session' => $result['Session'], - 'challenge_params' => $result['ChallengeParameters'], - 'username' => $credentials[$this->keyUsername], - 'user' => serialize($user) - ]; - break; - - default: - if (in_array($result['ChallengeName'], config('cognito.forced_challenge_names'))) { - //Check for forced action on challenge status - if (config('cognito.force_password_change_api')) { - $this->claim = [ - 'session' => $result['Session'], - 'username' => $credentials[$this->keyUsername], - 'status' => $result['ChallengeName'] - ]; - } else { - if (config('cognito.force_password_auto_update_api')) { - //Force set password same as authenticated with challenge state - $this->client->confirmPassword($credentials[$this->keyUsername], $credentials['password'], $result['Session']); - - //Get the result object again - $result = $this->client->authenticate($credentials[$this->keyUsername], $credentials['password']); - - //Create claim token - $this->claim = new AwsCognitoClaim($result, $user, $credentials[$this->keyUsername]); - - if (empty($result)) { - return false; - } //End if - } else { - $this->claim = null; - } //End if - } //End if - } //End if - break; - } //End switch - } else { //Create Claim for confirmed users - //Create claim token - $this->claim = new AwsCognitoClaim($result, $user, $credentials[$this->keyUsername]); - } //End if - - return ($this->claim)?true:false; - } else { - return false; - } //End if - } //Function ends - - /** * Attempt to authenticate a user using the given credentials. * @@ -176,26 +118,49 @@ protected function hasValidCredentials($user, array $credentials, bool $remember * @throws * @return bool */ - public function attempt(array $credentials = [], bool $remember = false) + public function attempt(array $request = [], $remember = false, string $paramUsername='email', string $paramPassword='password') { + $returnValue = null; try { - $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); - - //Check if the user exists in local data store - if (!($user instanceof Authenticatable)) { - throw new NoLocalUserException(); - } //End if - - if ($this->hasValidCredentials($user, $credentials)) { - return $this->login($user); + //convert to collection + $request = collect($request); + + //Build the payload + $payloadCognito = $this->buildCognitoPayload($request, $paramUsername, $paramPassword); + + //Check if the payload has valid AWS credentials + $responseCognito = collect($this->hasValidAWSCredentials($payloadCognito)); + if ($responseCognito) { + if ($this->claim) { + $credentials = collect([ + config('cognito.user_subject_uuid') => $this->claim->getSub() + ]); + + //Check if the user exists + $this->lastAttempted = $user = $this->hasValidLocalCredentials($credentials); + + //Login the user into the token guard + $returnValue = $this->login($user); + } elseif ($this->challengeName) { + //Get the key + $key = $this->challengeData['session_token']; + + //Save the challenge data + $this->setChallengeData($key); + + $returnValue = $this->challengeData; + } else { + throw new AwsCognitoException(); + } //End if + } else { + throw new InvalidUserException(); } //End if - - return false; } catch (NoLocalUserException $e) { - Log::error('CognitoTokenGuard:attempt:NoLocalUserException:'); + Log::error('CognitoTokenGuard:attempt:NoLocalUserException:'.$e->getMessage()); throw $e; } catch (CognitoIdentityProviderException $e) { Log::error('CognitoTokenGuard:attempt:CognitoIdentityProviderException:'.$e->getAwsErrorCode()); + $returnValue = $e->getAwsErrorCode(); //Set proper route if (!empty($e->getAwsErrorCode())) { @@ -214,7 +179,7 @@ public function attempt(array $credentials = [], bool $remember = false) break; } //End switch - return response()->json([ + $returnValue = response()->json([ 'error' => $errorCode, 'message' => $e->getAwsErrorMessage(), 'aws_error_code' => $e->getAwsErrorCode(), @@ -222,14 +187,16 @@ public function attempt(array $credentials = [], bool $remember = false) ], 400); } //End if - return $e->getAwsErrorCode(); - } catch (AwsCognitoException $e) { + return $returnValue; + } catch (AwsCognitoException | InvalidUserException $e) { Log::error('CognitoTokenGuard:attempt:AwsCognitoException:'. $e->getMessage()); throw $e; } catch (Exception $e) { Log::error('CognitoTokenGuard:attempt:Exception:'.$e->getMessage()); throw $e; } //Try-catch ends + + return $returnValue; } //Function ends @@ -240,20 +207,17 @@ public function attempt(array $credentials = [], bool $remember = false) * * @return claim */ - private function login($user) + private function login(Authenticatable $user) { if (!empty($this->claim)) { //Save the claim if it matches the Cognito Claim if ($this->claim instanceof AwsCognitoClaim) { + //Set User + $this->claim->setUser($user); //Set Token $this->setToken(); - } else { - $key = $this->claim['session']; - - //Save the challenge data - $this->setChallengeData($key, $user); } //End if //Set user @@ -309,9 +273,9 @@ public function getChallengeData(string $key) * * @return $this */ - public function setChallengeData(string $key, $user) + public function setChallengeData(string $key) { - $this->cognito->setChallengeData($key, $this->claim); + $this->cognito->setChallengeData($key, $this->challengeData); return $this; } //Function ends @@ -421,30 +385,42 @@ public function getUser (string $identifier) { /** * Attempt MFA based Authentication + * + * @param array $challenge + * @param bool $remember + * + * @throws + * + * @return bool */ - public function attemptMFA(array $challenge = [], Authenticatable $user, bool $remember=false) { + public function attemptMFA(array $challenge=[], bool $remember=false) { + $returnValue = null; try { - $response = $this->attemptBaseMFA($challenge, $user, $remember); - //Result of type AWS Result - if (!empty($response)) { - - //Handle the response as Aws Cognito Claim - if ($response instanceof AwsCognitoClaim) { - $this->claim = $response; - return $this->login($user); - } //End if - - //Handle if the object is a Aws Cognito Result - if ($response instanceof AwsResult) { - //Check in case of any challenge - if (isset($response['ChallengeName'])) { - //TODO: Handle challenge in MFA login - } //End if + $responseCognito = $this->attemptBaseMFA($challenge, $remember); + if ($responseCognito) { + if ($this->claim) { + $credentials = collect([ + config('cognito.user_subject_uuid') => $this->claim->getSub() + ]); + + //Check if the user exists + $this->lastAttempted = $user = $this->hasValidLocalCredentials($credentials); + + //Login the user into the token guard + $returnValue = $this->login($user); + } elseif ($this->challengeName) { + $returnValue = $this->challengeData; + } else { + throw new AwsCognitoException(); } //End if + } else { + throw new InvalidUserException(); } //End if - } catch(Exception $e) { + } catch(AwsCognitoException | InvalidUserException | Exception $e) { throw $e; } //Try-catch ends + + return $returnValue; } //Function ends } //Class ends diff --git a/src/Guards/Traits/BaseCognitoGuard.php b/src/Guards/Traits/BaseCognitoGuard.php index 988cc0c..b7f3cd1 100644 --- a/src/Guards/Traits/BaseCognitoGuard.php +++ b/src/Guards/Traits/BaseCognitoGuard.php @@ -11,6 +11,23 @@ namespace Ellaisys\Cognito\Guards\Traits; +use Aws\Result as AwsResult; + +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; +use Illuminate\Contracts\Auth\Authenticatable; + +use Ellaisys\Cognito\AwsCognito; +use Ellaisys\Cognito\AwsCognitoClaim; +use Ellaisys\Cognito\AwsCognitoClient; +use Ellaisys\Cognito\AwsCognitoClientInterface; +use Ellaisys\Cognito\AwsCognitoClientManager; + +use Exception; +use Ellaisys\Cognito\Exceptions\NoLocalUserException; +use Ellaisys\Cognito\Exceptions\InvalidUserException; +use Ellaisys\Cognito\Validators\AwsCognitoTokenValidator; + /** * Trait Base Cognito Guard */ @@ -19,7 +36,7 @@ trait BaseCognitoGuard /** * Get the AWS Cognito object - * + * * @return \Ellaisys\Cognito\AwsCognito */ public function cognito() { @@ -29,11 +46,277 @@ public function cognito() { /** * Get the User Information from AWS Cognito - * + * * @return mixed */ public function getRemoteUserData(string $username) { return $this->client->getUser($username); } //Function ends -} //Trait ends \ No newline at end of file + + /** + * Set the User Information into the local DB + * + * @return mixed + */ + public function setLocalUserData(array $credentials) { + try { + //Get username key in the credentials + $keyUsername = config('cognito.cognito_user_fields.email', 'email'); + + //Get user from AWS Cognito + $remoteUser = $this->getRemoteUserData($credentials[$keyUsername]); + if (!empty($remoteUser)) { + //Get user from presisting store + $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); + } else { + throw new InvalidUserException('User not found in AWS Cognito'); + } //End if + + //Check if the user is not empty + if (config('cognito.add_missing_local_user', false)) { + //Create user object from AWS Cognito + $user = []; + + //Create user into local DB + $this->provider->createUser($user); + } else { + return null; + } //End if + + } catch (InvalidUserException | Exception $e) { + Log::debug('BaseCognitoGuard:setLocalUserData:Exception:'); + throw $e; + } //End try-catch + + return $this->client->getUser($username); + } //Function ends + + + /** + * Validate the user credentials with AWS Cognito + * + * @return \Ellaisys\Cognito\AwsCognitoClient + */ + protected function hasValidAWSCredentials(Collection $credentials) { + //Reset global variables + $this->challengeName = null; + $this->challengeData = null; + $this->claim = null; + $this->awsResult = null; + + //Authenticate the user with AWS Cognito + $result = $this->client->authenticate($credentials['email'], $credentials['password']); + + //Check if the result is an instance of AwsResult + if (!empty($result) && $result instanceof AwsResult) { + //Set value into class param + $this->awsResult = $result; + + //Check in case of any challenge + if (isset($result['ChallengeName'])) { + $this->challengeName = $result['ChallengeName']; + $this->challengeData = $this->handleCognitoChallenge($result, $credentials['email']); + } elseif (isset($result['AuthenticationResult'])) { + //Create claim token + $this->claim = new AwsCognitoClaim($result, null); + } else { + $result = null; + } //End if + } //End if + + return $result; + } //Function ends + + + /** + * handle Cognito Challenge + */ + protected function handleCognitoChallenge(AwsResult $result, string $username) { + + //Return value + $returnValue = null; + + switch ($result['ChallengeName']) { + case 'SOFTWARE_TOKEN_MFA': + $returnValue = [ + 'status' => $result['ChallengeName'], + 'session_token' => $result['Session'], + 'username' => $username + ]; + break; + + case 'SMS_MFA': + case 'SELECT_MFA_TYPE': + $returnValue = [ + 'status' => $result['ChallengeName'], + 'session_token' => $result['Session'], + 'challenge_params' => $result['ChallengeParameters'], + 'username' => $username + ]; + break; + + default: + if (in_array($result['ChallengeName'], config('cognito.forced_challenge_names'))) { + $returnValue = $result['ChallengeName']; + } //End if + break; + } //End switch + + return $returnValue; + } //Function ends + + + /** + * Build the payload array. + * + * @param \Illuminate\Http\Request $request + * @param string $paramUsername + * @param string $paramPassword + * @return array + */ + final public function buildCognitoPayload(Collection $request, $paramUsername='email', $paramPassword='password', bool $isCredential=false): Collection + { + $payload = []; + + //Get key fields + if ($isCredential) { + $rememberMe = $request->has('remember')?$request['remember']:false; + $payload = array_merge($payload, ['remember' => $rememberMe]); + } //End if + + //Get the configuration fields + $userFields = array_filter(config('cognito.cognito_user_fields'), function($value) { + return !empty($value); + }); + + if ($userFields) { + //Iterate all the keys in the request + $request->each(function($value, $key) use ($userFields, $paramUsername, $paramPassword, &$payload, $isCredential) { + switch ($key) { + case $paramUsername: + $payload = array_merge($payload, ['email' => $value]); + break; + + case $paramPassword: + $payload = array_merge($payload, ['password' => $value]); + break; + + default: + if ($isCredential && array_key_exists($key, $userFields)) { + $payload = array_merge($payload, [$key => $value]); + } + break; + } //Switch ends + }); + } //End if + + return collect($payload); + } //Function ends + + + /** + * Validate the user credentials with Local Data Store + * + * @return \Illuminate\Contracts\Auth\Authenticatable + */ + protected function hasValidLocalCredentials(Collection $credentials): Authenticatable { + try { + $user = $this->provider->retrieveByCredentials($credentials->toArray()); + + //Check if the user is not empty + if (empty($user) && !($user instanceof Authenticatable)) { + if (config('cognito.add_missing_local_user')) { + //Fetch user data from cognito + $userRemote = $this->getRemoteUserData($this->claim->getUsername()); + if (empty($userRemote)) { + throw new InvalidUserException(); + } //End if + + //Create user object from cognito data + $payloadUser = $this->buildLocalUserPayload(collect($userRemote['UserAttributes'])); + + //Create user into local DB + if ($this->createLocalUser($payloadUser->toArray())) { + $user = $this->provider->retrieveByCredentials($credentials->toArray()); + } //End if + } else { + throw new NoLocalUserException(); + } //End if + } //End if + + return $user; + } catch (InvalidUserException | NoLocalUserException | Exception $e) { + Log::debug('BaseCognitoGuard:setLocalUserData:Exception'); + throw $e; + } //End try-catch + } //Function ends + + + /** + * Build the payload for Local DB + * + * @param \Illuminate\Http\Request $request + * @param string $paramUsername + * @param string $paramPassword + * @return array + */ + final public function buildLocalUserPayload(Collection $request): Collection + { + $payload = []; + + try { + //Get the configuration fields + $userFields = array_filter(config('cognito.cognito_user_fields'), function($value) { + return !empty($value); + }); + + if ($userFields) { + //Iterate all the keys in the request + $request->each(function($value) use ($userFields, &$payload) { + if (array_key_exists($value['Name'], $userFields)) { + $payload = array_merge($payload, [$userFields[$value['Name']] => $value['Value']]); + } //End if + + //Add user subject if exists + if ($value['Name'] == 'sub') { + $payload = array_merge($payload, [config('cognito.user_subject_uuid') => $value['Value']]); + } //End if + }); + } //End if + + } catch (Exception $e) { + Log::error($e->getMessage()); + throw $e; + } //End try-catch + + return collect($payload); + } //Function ends + + + /** + * Create a local user if one does not exist. + * + * @param array $credentials + * @return mixed + */ + final public function createLocalUser(array $dataUser, string $keyPassword='password') + { + $user = null; + if (config('cognito.add_missing_local_user')) { + //Get user model from configuration + $userModel = config('cognito.sso_user_model'); + + //Remove password from credentials if exists + if (array_key_exists($keyPassword, $dataUser)) { + unset($dataUser[$keyPassword]); + } //End if + + //Create user into local DB, if not exists + $user = $userModel::updateOrCreate($dataUser); + } //End if + + return $user; + } //Function ends + +} //Trait ends diff --git a/src/Guards/Traits/CognitoMFA.php b/src/Guards/Traits/CognitoMFA.php index 86c854c..100f953 100644 --- a/src/Guards/Traits/CognitoMFA.php +++ b/src/Guards/Traits/CognitoMFA.php @@ -34,31 +34,41 @@ trait CognitoMFA /** * Attempt MFA based Authentication */ - public function attemptBaseMFA(array $challenge = [], Authenticatable $user, bool $remember=false) { + public function attemptBaseMFA(array $challenge = [], bool $remember=false) { try { - $claim = null; + //Reset global variables + $this->challengeName = null; + $this->challengeData = null; + $this->claim = null; + $this->awsResult = null; $challengeName = $challenge['challenge_name']; $session = $challenge['session']; $challengeValue = $challenge['mfa_code']; $username = $challenge['username']; + //Attempt MFA Challenge $result = $this->client->authMFAChallenge($challengeName, $session, $challengeValue, $username); - //Result of type AWS Result + + //Check if the result is an instance of AwsResult if (!empty($result) && $result instanceof AwsResult) { + //Set value into class param + $this->awsResult = $result; + //Check in case of any challenge - if (isset($result['ChallengeName'])) { - return $result; - } else { + if (isset($result['ChallengeName'])) { + $this->challengeName = $result['ChallengeName']; + $this->challengeData = $this->handleCognitoChallenge($result, $username); + } elseif (isset($result['AuthenticationResult'])) { //Create claim token - return new AwsCognitoClaim($result, $user, $username); + $this->claim = new AwsCognitoClaim($result, null); + } else { + throw new HttpException(400, 'ERROR_AWS_COGNITO_MFA_CODE_NOT_PROPER'); } //End if - } else { - throw new HttpException(400, 'ERROR_AWS_COGNITO_MFA_CODE_NOT_PROPER'); } //End if - } catch(CognitoIdentityProviderException $e) { - throw $e; - } catch(Exception $e) { + + return $result; + } catch(CognitoIdentityProviderException | Exception $e) { throw $e; } //Try-catch ends } //Function ends @@ -66,7 +76,7 @@ public function attemptBaseMFA(array $challenge = [], Authenticatable $user, boo /** * Associate the MFA Software Token - * + * * @param string $appName (optional) * * @return array diff --git a/src/Providers/AwsCognitoServiceProvider.php b/src/Providers/AwsCognitoServiceProvider.php index f5082f1..c353e14 100644 --- a/src/Providers/AwsCognitoServiceProvider.php +++ b/src/Providers/AwsCognitoServiceProvider.php @@ -165,7 +165,7 @@ protected function registerCognitoFacades() ); }); - $this->app->singleton('ellaisys.aws.cognito', function (Application $app, array $config) { + $this->app->singleton('ellaisys.aws.cognito', function (Application $app) { return new AwsCognito( $app['ellaisys.aws.cognito.manager'], $app['ellaisys.aws.cognito.parser'] @@ -189,7 +189,7 @@ protected function registerCognitoFacades() */ protected function registerCognitoProvider() { - $this->app->singleton(AwsCognitoClient::class, function (Application $app) { + $this->app->singleton(AwsCognitoClient::class, function () { $aws_config = [ 'region' => config('cognito.region'), 'version' => config('cognito.version') @@ -202,15 +202,13 @@ protected function registerCognitoProvider() } //End if //Instancite the AWS Cognito Client - $client = new AwsCognitoClient( + return new AwsCognitoClient( new CognitoIdentityProviderClient($aws_config), config('cognito.app_client_id'), config('cognito.app_client_secret'), config('cognito.user_pool_id'), config('cognito.app_client_secret_allow', true) ); - - return $client; }); $this->app->singleton(AwsCognitoUserPool::class, function (Application $app) { @@ -230,7 +228,7 @@ protected function extendWebAuthGuard() $guard = new CognitoSessionGuard( $name, $app['ellaisys.aws.cognito'], - $client = $app->make(AwsCognitoClient::class), + $app->make(AwsCognitoClient::class), $app['auth']->createUserProvider($config['provider']), $app['session.store'], $app['request'] @@ -256,7 +254,7 @@ protected function extendApiAuthGuard() $guard = new CognitoTokenGuard( $app['ellaisys.aws.cognito'], - $client = $app->make(AwsCognitoClient::class), + $app->make(AwsCognitoClient::class), $app['request'], Auth::createUserProvider($config['provider']), config('cognito.cognito_user_fields.email', 'email') diff --git a/src/Providers/StorageProvider.php b/src/Providers/StorageProvider.php index d2e543e..0afa71e 100644 --- a/src/Providers/StorageProvider.php +++ b/src/Providers/StorageProvider.php @@ -13,6 +13,7 @@ use Illuminate\Support\Facades\Cache; use Psr\SimpleCache\CacheInterface as PsrCacheInterface; +use BadMethodCallException; class StorageProvider { @@ -71,7 +72,7 @@ public function add($key, $value, $duration=3600) { // If the laravel version is 5.8 or higher then convert minutes to seconds. if ($this->laravelVersion !== null - && is_int($minutes) + && is_int($duration) && version_compare($this->laravelVersion, '5.8', '<') ) { $duration = ($duration/60); @@ -209,4 +210,4 @@ protected function determineTagSupport() } } //Function ends -} //Class ends \ No newline at end of file +} //Class ends diff --git a/src/Traits/AwsCognitoClientAdminAction.php b/src/Traits/AwsCognitoClientAdminAction.php index 1241fef..e48bb87 100644 --- a/src/Traits/AwsCognitoClientAdminAction.php +++ b/src/Traits/AwsCognitoClientAdminAction.php @@ -13,6 +13,10 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; +use Aws\CognitoIdentityProvider\CognitoIdentityProviderClient; +use Aws\CognitoIdentityProvider\Exception\InvalidPasswordException; +use Aws\CognitoIdentityProvider\Exception\NotAuthorizedException ; +use Aws\CognitoIdentityProvider\Exception\CognitoIdentityProviderException; /** * WS Cognito Client for AWS Admin Users @@ -79,7 +83,7 @@ public function adminDisableUser(string $username) /** - * Signs out a user from all devices. It also invalidates all refresh tokens that Amazon Cognito has + * Signs out a user from all devices. It also invalidates all refresh tokens that Amazon Cognito has * issued to a user. The user's current access and ID tokens remain valid until they expire. * * @see https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-cognito-idp-2016-04-18.html#adminuserglobalsignout @@ -162,4 +166,4 @@ public function describeUserPool() return true; } //Function ends -} //Trait ends \ No newline at end of file +} //Trait ends