diff --git a/app/Http/Controllers/Auth/EmailVerificationController.php b/app/Http/Controllers/Auth/EmailVerificationController.php index 14c75260..9711343a 100644 --- a/app/Http/Controllers/Auth/EmailVerificationController.php +++ b/app/Http/Controllers/Auth/EmailVerificationController.php @@ -12,6 +12,7 @@ * limitations under the License. **/ use App\Http\Controllers\Controller; +use App\libs\Utils\EmailUtils; use App\Services\Auth\IUserService; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Redirect; @@ -46,7 +47,7 @@ public function showVerificationForm(LaravelRequest $request) $params = ['email' => '']; if($request->has("email")){ $email = trim($request->get("email")); - if (filter_var($email, FILTER_VALIDATE_EMAIL) !== FALSE) { + if (EmailUtils::isValidEmail($email)) { $params['email'] = $email; } } diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 2dca111a..43ef3aeb 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -13,6 +13,7 @@ **/ use App\Http\Controllers\Controller; +use App\libs\Utils\EmailUtils; use App\Services\Auth\IUserService; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; @@ -68,7 +69,7 @@ public function showLinkRequestForm(LaravelRequest $request) if($request->has("email")){ $email = trim($request->get("email")); - if (filter_var($email, FILTER_VALIDATE_EMAIL) !== FALSE) { + if (EmailUtils::isValidEmail($email)) { $params['email'] = $email; } } diff --git a/app/Http/Middleware/ETagsMiddleware.php b/app/Http/Middleware/ETagsMiddleware.php index f7c4abf1..627357a8 100644 --- a/app/Http/Middleware/ETagsMiddleware.php +++ b/app/Http/Middleware/ETagsMiddleware.php @@ -1,6 +1,6 @@ getMethod(); + + // Support using HEAD method for checking If-None-Match + if ($request->isMethod('HEAD')) { + $request->setMethod('GET'); + } + //Handle response $response = $next($request); + if ($response->getStatusCode() === 200 && $request->getMethod() === 'GET') { - $etag = md5($response->getContent()); + $etag = md5($response->getContent()); $requestETag = str_replace('"', '', $request->getETags()); $requestETag = str_replace('-gzip', '', $requestETag); + if($requestETag && is_array($requestETag)) + Log::debug(sprintf("ETagsMiddleware::handle requestEtag %s calculated etag %s", $requestETag[0], $etag)); - if ($requestETag && $requestETag[0] == $etag) - { - Log::debug('ETAG 304'); + // Strip W/ if weak comparison algorithm can be used + $requestETag = array_map([$this, 'stripWeakTags'], $requestETag); + + if (in_array($etag, $requestETag)) { $response->setNotModified(); } + $response->setEtag($etag); } + $request->setMethod($method); + return $response; } + + private function stripWeakTags($etag) + { + return str_replace('W/', '', $etag); + } } \ No newline at end of file diff --git a/app/Http/Utils/Filters/Filter.php b/app/Http/Utils/Filters/Filter.php index 2751ff3b..7a6992ff 100644 --- a/app/Http/Utils/Filters/Filter.php +++ b/app/Http/Utils/Filters/Filter.php @@ -11,6 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +use App\libs\Utils\PunnyCodeHelper; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\QueryBuilder; use Illuminate\Support\Facades\Validator; @@ -349,7 +351,7 @@ public function apply2Query(QueryBuilder $query, array $mappings) * @param string $original_format * @return mixed */ - private function convertValue($value, $original_format) + private function convertValue(string $value, string $original_format) { switch ($original_format) { case 'datetime_epoch': @@ -360,8 +362,10 @@ private function convertValue($value, $original_format) return intval($value); break; case 'json_string': - return sprintf("%s",$value); + return sprintf("%s", $value); break; + case 'json_email': + return PunnyCodeHelper::encodeEmail($value); default: return $value; break; diff --git a/app/Http/Utils/ParseMultiPartFormDataInputStream.php b/app/Http/Utils/ParseMultiPartFormDataInputStream.php index a95b3046..ee5e2416 100644 --- a/app/Http/Utils/ParseMultiPartFormDataInputStream.php +++ b/app/Http/Utils/ParseMultiPartFormDataInputStream.php @@ -158,9 +158,13 @@ private function decide($string) private function file($string) { preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match); - return [ - $match[1] => ($match[2] !== NULL ? $match[2] : '') - ]; + if(count($match) >=2 ) { + return [ + $match[1] => ($match[2] !== NULL ? $match[2] : '') + ]; + } + Log::warning(sprintf( "ParseMultiPartFormDataInputStream::file %s", $string)); + return []; } /** diff --git a/app/Models/OAuth2/OAuth2OTP.php b/app/Models/OAuth2/OAuth2OTP.php index 07a2cd6e..3216b357 100644 --- a/app/Models/OAuth2/OAuth2OTP.php +++ b/app/Models/OAuth2/OAuth2OTP.php @@ -11,6 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +use App\libs\Utils\PunnyCodeHelper; use App\Models\Utils\BaseEntity; use Doctrine\ORM\Mapping AS ORM; use DateTime; @@ -192,7 +194,7 @@ public function setScope(?string $scope): void */ public function getEmail(): ?string { - return $this->email; + return PunnyCodeHelper::decodeEmail($this->email); } /** @@ -200,7 +202,7 @@ public function getEmail(): ?string */ public function setEmail(?string $email): void { - $this->email = !empty($email) ? strtolower(trim($email)):null; + $this->email = PunnyCodeHelper::encodeEmail($email); } /** @@ -361,7 +363,7 @@ public function isValid():bool{ } public function getUserName():?string{ - return $this->connection == OAuth2Protocol::OAuth2PasswordlessEmail ? $this->email : $this->phone_number; + return $this->connection == OAuth2Protocol::OAuth2PasswordlessEmail ? $this->getEmail() : $this->phone_number; } /** @@ -402,7 +404,7 @@ public function generateValue(): string public static function fromRequest(OAuth2AccessTokenRequestPasswordless $request, int $length):OAuth2OTP{ $instance = new self($length); $instance->connection = $request->getConnection(); - $instance->email = $request->getEmail(); + $instance->setEmail($request->getEmail()); $instance->phone_number = $request->getPhoneNumber(); $instance->scope = $request->getScopes(); $instance->setValue($request->getOTP()); @@ -420,7 +422,7 @@ public static function fromParams(string $user_name, string $connection, string $instance = new self(strlen($value)); $instance->connection = $connection; if($connection == OAuth2Protocol::OAuth2PasswordlessConnectionEmail) - $instance->email = $user_name; + $instance->setEmail($user_name); if($connection == OAuth2Protocol::OAuth2PasswordlessConnectionEmail) $instance->phone_number = $user_name; $instance->setValue($value); diff --git a/app/Repositories/DoctrineUserRepository.php b/app/Repositories/DoctrineUserRepository.php index 0711f41b..7763e4b9 100644 --- a/app/Repositories/DoctrineUserRepository.php +++ b/app/Repositories/DoctrineUserRepository.php @@ -11,6 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +use App\libs\Utils\PunnyCodeHelper; use Auth\Repositories\IUserRepository; use Auth\User; use utils\DoctrineFilterMapping; @@ -49,8 +51,8 @@ protected function getFilterMappings() 'last_name' => 'e.last_name:json_string', 'full_name' => new DoctrineFilterMapping("concat(e.first_name, ' ', e.last_name) :operator :value"), 'github_user' => 'e.github_user:json_string', - 'email' => ['e.email:json_string', 'e.second_email:json_string', 'e.third_email:json_string'], - 'primary_email' => 'e.email:json_string', + 'email' => ['e.email:json_email', 'e.second_email:json_email', 'e.third_email:json_email'], + 'primary_email' => 'e.email:json_email', 'active' => 'e.active:json_boolean', 'group_id' => new DoctrineJoinFilterMapping('e.groups', "g", "g.id :operator :value") ]; @@ -87,12 +89,14 @@ public function getByToken(string $token): ?User */ public function getByEmailOrName(string $term): ?User { + $term = PunnyCodeHelper::encodeEmail($term); + return $this->getEntityManager() ->createQueryBuilder() ->select("e") ->from($this->getBaseEntity(), "e") ->Where("e.email = (:term)") - ->setParameter("term", trim($term)) + ->setParameter("term", $term) ->getQuery() ->getOneOrNullResult(); } diff --git a/app/libs/Auth/CustomAuthProvider.php b/app/libs/Auth/CustomAuthProvider.php index 95d64c3d..7fb90144 100644 --- a/app/libs/Auth/CustomAuthProvider.php +++ b/app/libs/Auth/CustomAuthProvider.php @@ -118,7 +118,8 @@ public function retrieveByCredentials(array $credentials) $email = $credentials['username']; $password = $credentials['password']; - $user = $this->user_repository->getByEmailOrName(trim($email)); + + $user = $this->user_repository->getByEmailOrName($email); if (is_null($user)) //user must exists { @@ -193,7 +194,7 @@ public function validateCredentials(Authenticatable $user, array $credentials) $email = $credentials['username']; $password = $credentials['password']; - $user = $this->user_repository->getByEmailOrName(trim($email)); + $user = $this->user_repository->getByEmailOrName($email); if (!$user || !$user->canLogin() || !$user->checkPassword($password)) { return false; diff --git a/app/libs/Auth/Factories/UserFactory.php b/app/libs/Auth/Factories/UserFactory.php index 656e1f15..89e1d30f 100644 --- a/app/libs/Auth/Factories/UserFactory.php +++ b/app/libs/Auth/Factories/UserFactory.php @@ -50,7 +50,7 @@ public static function populate(User $user, array $payload):User{ $user->setLastName(trim($payload['last_name'])); if(isset($payload['email']) && !empty($payload['email'])) - $user->setEmail(strtolower(trim($payload['email']))); + $user->setEmail($payload['email']); if(isset($payload['second_email'])) $user->setSecondEmail(strtolower(trim($payload['second_email']))); @@ -62,7 +62,7 @@ public static function populate(User $user, array $payload):User{ $user->setBio(trim($payload['bio'])); if(isset($payload['identifier']) && !empty($payload['identifier'])) - $user->setIdentifier(trim($payload['identifier'])); + $user->setIdentifier($payload['identifier']); if(isset($payload['statement_of_interest'])) $user->setStatementOfInterest(trim($payload['statement_of_interest'])); diff --git a/app/libs/Auth/Models/User.php b/app/libs/Auth/Models/User.php index 32570e09..ef079a4e 100644 --- a/app/libs/Auth/Models/User.php +++ b/app/libs/Auth/Models/User.php @@ -16,6 +16,7 @@ use App\Events\UserSpamStateUpdated; use App\libs\Auth\Models\IGroupSlugs; use App\libs\Auth\Models\UserRegistrationRequest; +use App\libs\Utils\PunnyCodeHelper; use Doctrine\ORM\Event\PreUpdateEventArgs; use GuzzleHttp\Exception\RequestException; use Illuminate\Support\Facades\Cache; @@ -499,12 +500,12 @@ public function getAuthPassword() */ public function getIdentifier(): ?string { - return $this->identifier; + return PunnyCodeHelper::decodeEmail($this->identifier); } public function getEmail():string { - return $this->email; + return PunnyCodeHelper::decodeEmail($this->email); } /** @@ -513,7 +514,7 @@ public function getEmail():string public function getFullName(): ?string { $full_name = $this->getFirstName() . " " . $this->getLastName(); - return !empty(trim($full_name)) ? $full_name : $this->email; + return !empty(trim($full_name)) ? $full_name : $this->getEmail(); } public function getFirstName() @@ -919,7 +920,7 @@ public function setPic(string $pic){ private function getGravatarUrl(): string { $url = 'https://www.gravatar.com/avatar/'; - $url .= md5(strtolower(trim($this->email))); + $url .= md5($this->getEmail()); return $url; } @@ -932,13 +933,13 @@ public function checkPassword(string $password): bool { if(empty($this->password)) { - Log::warning(sprintf("User %s (%s) has not password set.", $this->id, $this->email)); + Log::warning(sprintf("User %s (%s) has not password set.", $this->id, $this->getEmail())); return false; } if(empty($this->password_enc)) { - Log::warning(sprintf("User %s (%s) has not password encoding set.", $this->id, $this->email)); + Log::warning(sprintf("User %s (%s) has not password encoding set.", $this->id, $this->getEmail())); return false; } @@ -1174,7 +1175,7 @@ public function setCountryIsoCode(string $country_iso_code): void */ public function getSecondEmail(): ?string { - return $this->second_email; + return PunnyCodeHelper::decodeEmail($this->second_email); } /** @@ -1182,7 +1183,7 @@ public function getSecondEmail(): ?string */ public function setSecondEmail(string $second_email): void { - $this->second_email = $second_email; + $this->second_email = PunnyCodeHelper::encodeEmail($second_email); } /** @@ -1190,7 +1191,7 @@ public function setSecondEmail(string $second_email): void */ public function getThirdEmail(): ?string { - return $this->third_email; + return PunnyCodeHelper::decodeEmail($this->third_email); } /** @@ -1198,7 +1199,7 @@ public function getThirdEmail(): ?string */ public function setThirdEmail(string $third_email): void { - $this->third_email = $third_email; + $this->third_email = PunnyCodeHelper::encodeEmail($third_email); } /** @@ -1529,7 +1530,8 @@ public function setLastName(string $last_name): void */ public function setEmail(string $email): void { - $email = trim($email); + $email = PunnyCodeHelper::encodeEmail($email); + if (!empty($this->email) && $email != $this->email) { //we are setting a new email $this->clearResetPasswordRequests(); @@ -1584,12 +1586,13 @@ public function verifyEmail(bool $send_email_verified_notice = true) { if (!$this->email_verified) { - Log::debug(sprintf("User::verifyEmail verifying email %s", $this->email)); + Log::debug(sprintf("User::verifyEmail verifying email %s", $this->getEmail())); $this->email_verified = true; $this->spam_type = self::SpamTypeHam; $this->active = true; $this->lock = false; $this->email_verified_date = new \DateTime('now', new \DateTimeZone('UTC')); + if($send_email_verified_notice) Event::dispatch(new UserEmailVerified($this->getId())); Event::dispatch(new UserSpamStateUpdated($this->getId())); @@ -1604,7 +1607,7 @@ public function verifyEmail(bool $send_email_verified_notice = true) public function generateEmailVerificationToken(): string { if($this->isEmailVerified()){ - throw new ValidationException(sprintf("User %s (%s) is already verified.", $this->id, $this->email)); + throw new ValidationException(sprintf("User %s (%s) is already verified.", $this->id, $this->getEmail())); } $generator = new RandomGenerator(); @@ -1644,7 +1647,7 @@ public function setLanguage(string $language): void */ public function setIdentifier(string $identifier) { - $this->identifier = $identifier; + $this->identifier = PunnyCodeHelper::encodeEmail($identifier); } /** @@ -1674,7 +1677,7 @@ public function preUpdate(PreUpdateEventArgs $args) $email_changed = $args->hasChangedField("email"); if( $bio_changed|| $email_changed) { // enqueue user for spam re checker - Log::warning(sprintf("User::preUpdate user %s was marked for spam type reclasification.", $this->email)); + Log::warning(sprintf("User::preUpdate user %s was marked for spam type reclasification.", $this->getEmail())); $this->resetSpamTypeClassification(); Event::dispatch(new UserSpamStateUpdated($this->getId())); } diff --git a/app/libs/Auth/Models/UserRegistrationRequest.php b/app/libs/Auth/Models/UserRegistrationRequest.php index 1e3f90f3..0c0698d7 100644 --- a/app/libs/Auth/Models/UserRegistrationRequest.php +++ b/app/libs/Auth/Models/UserRegistrationRequest.php @@ -11,6 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +use App\libs\Utils\PunnyCodeHelper; use App\Models\Utils\BaseEntity; use Auth\User; use Doctrine\ORM\Mapping AS ORM; @@ -126,7 +128,7 @@ public function setHash(string $hash): void */ public function getEmail(): string { - return $this->email; + return PunnyCodeHelper::decodeEmail($this->email); } /** @@ -134,7 +136,7 @@ public function getEmail(): string */ public function setEmail(string $email): void { - $this->email = $email; + $this->email = PunnyCodeHelper::encodeEmail($email); } /** diff --git a/app/libs/OAuth2/GrantTypes/InteractiveGrantType.php b/app/libs/OAuth2/GrantTypes/InteractiveGrantType.php index a79b1b12..bae821ba 100644 --- a/app/libs/OAuth2/GrantTypes/InteractiveGrantType.php +++ b/app/libs/OAuth2/GrantTypes/InteractiveGrantType.php @@ -11,6 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +use App\libs\Utils\EmailUtils; +use App\libs\Utils\PunnyCodeHelper; use Exception; use Illuminate\Support\Facades\Log; use jwe\IJWE; @@ -425,7 +428,7 @@ protected function processUserHint(OAuth2AuthenticationRequest $request) if(!empty ($login_hint)) { - if (filter_var($login_hint, FILTER_VALIDATE_EMAIL)) + if (EmailUtils::isValidEmail($login_hint)) { $user = $this->auth_service->getUserByUsername($login_hint); } diff --git a/app/libs/Utils/EmailUtils.php b/app/libs/Utils/EmailUtils.php new file mode 100644 index 00000000..c46e50ad --- /dev/null +++ b/app/libs/Utils/EmailUtils.php @@ -0,0 +1,33 @@ + 'sebastian.marcet', ], + [ + 'first_name' => 'Sebastian', + 'last_name' => 'Marcet IDN', + 'email' => 'hei@やる.ca', + 'password' => '1qaz2wsx', + 'password_enc' => \Auth\AuthHelper::AlgSHA1_V2_4, + 'gender' => 'male', + 'address1' => 'Av. Siempre Viva 111', + 'address2' => 'Av. Siempre Viva 111', + 'city' => 'Lanus Este', + 'state' => 'Buenos Aires', + 'post_code' => '1824', + 'country' => 'AR', + 'language' => 'ESP', + 'active' => true, + 'email_verified' => true, + 'groups' => [ + $super_admin_group + ], + 'identifier' => 'hei@やる', + ], [ 'first_name' => 'Márton', 'last_name' => 'Kiss', diff --git a/tests/OAuth2UserRegistrationServiceApiTest.php b/tests/OAuth2UserRegistrationServiceApiTest.php index 3ad873f5..d07dc93a 100644 --- a/tests/OAuth2UserRegistrationServiceApiTest.php +++ b/tests/OAuth2UserRegistrationServiceApiTest.php @@ -56,6 +56,45 @@ public function testRegisterUserRequestCreation() $this->assertTrue(!empty($user_registration_request->hash)); } + public function testIDNRegisterUserRequestCreation() + { + $data = [ + 'email' => 'hei2@やる.ca', + 'first_name' => 'test_'. str_random(16), + 'last_name' => 'test_'. str_random(16), + ]; + + $params = [ + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json", + "Origin" => 'test.com' + ]; + + $response = $this->action + ( + "POST", + "Api\\OAuth2\\OAuth2UserRegistrationRequestApiController@register", + $params, + [], + [], + [], + $headers, + json_encode($data) + ); + + $content = $response->getContent(); + + $this->assertResponseStatus(201); + + $user_registration_request = json_decode($content); + + $this->assertTrue(!empty($user_registration_request->hash)); + $this->assertTrue($user_registration_request->email === 'hei2@やる.ca'); + } + public function testRegistrationRequestUpdate() { $headers = [ diff --git a/tests/OIDCProtocolTest.php b/tests/OIDCProtocolTest.php index b3a2cafa..05a9256d 100644 --- a/tests/OIDCProtocolTest.php +++ b/tests/OIDCProtocolTest.php @@ -389,6 +389,90 @@ public function testAuthCode() } + public function testAuthCodeIDN() + { + + $client_id = '%2E%2D%5F%7E87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'hei@やる.ca', + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200 + ); + + $response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth", + $params, + [], + [], + []); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'hei@やる.ca')); + + // do login + $response = $this->action('POST', "UserController@postLogin", + array + ( + 'username' => 'hei@やる.ca', + 'password' => '1qaz2wsx', + 'flow' => 'password', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2\OAuth2ProviderController@auth", + [], + [], + [], + []); + + $this->assertResponseStatus(302); + + //do consent + $url = $response->getTargetUrl(); + + $response = $this->action('POST', "UserController@postConsent", array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2\OAuth2ProviderController@auth", + [], + [], + [], + []); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = []; + parse_str($query, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + } + public function testAuthCodeInvalidLoginHint() {