Skip to content

Commit

Permalink
Merge pull request #104 from ellaisys/hotfix
Browse files Browse the repository at this point in the history
Hotfix
  • Loading branch information
amitdhongde authored Jul 11, 2024
2 parents 5d33b7c + 1a4c2ea commit bd69e74
Show file tree
Hide file tree
Showing 18 changed files with 798 additions and 476 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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 }}
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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_.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -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/",
Expand Down
26 changes: 23 additions & 3 deletions config/cognito.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
Expand Down Expand Up @@ -104,7 +124,7 @@
|--------------------------------------------------------------------------
|
| This option controls the cognito MFA configuration for the assigned user.
|
|
|
| MFA_NONE, MFA_ENABLED
|
Expand Down Expand Up @@ -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'),
Expand Down
13 changes: 13 additions & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -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
111 changes: 36 additions & 75 deletions src/Auth/AuthenticatesUsers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);

Expand All @@ -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


Expand Down Expand Up @@ -156,15 +153,13 @@ 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'])) {
//Get stored session
$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
Expand All @@ -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);
Expand All @@ -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
*
Expand All @@ -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


Expand Down
Loading

0 comments on commit bd69e74

Please sign in to comment.