From 4963dcd8b09217e96dc176c4b82da0fab048676e Mon Sep 17 00:00:00 2001 From: Dan Stoudt Date: Fri, 3 Aug 2018 13:43:28 -0400 Subject: [PATCH] Mobile Story - Add JWT Support, implement file upload, browser view, and login --- app/config/default/parameters.yml.dist | 3 + composer.json | 3 +- composer.lock | 78 +++++++++++---- .../CoreBundle/Service/Auth/CaseboxAuth.php | 8 +- src/Casebox/CoreBundle/Service/System.php | 7 +- src/Casebox/CoreBundle/Service/User.php | 2 +- .../Controller/RestApiController.php | 95 ++++++++++++++++--- .../RestBundle/Service/RestApiService.php | 79 ++++++++++++++- 8 files changed, 234 insertions(+), 41 deletions(-) diff --git a/app/config/default/parameters.yml.dist b/app/config/default/parameters.yml.dist index 79a54866..8817df2e 100755 --- a/app/config/default/parameters.yml.dist +++ b/app/config/default/parameters.yml.dist @@ -35,3 +35,6 @@ parameters: converter: 'api' # api|unoconv redis_host: 127.0.0.1 redis_port: 6379 + jwt_key: ~ + jwt_algorithm: HS256 + jwt_expiration_seconds: 600 \ No newline at end of file diff --git a/composer.json b/composer.json index a458721c..841e2d61 100755 --- a/composer.json +++ b/composer.json @@ -45,7 +45,8 @@ "reprovinci/solr-php-client": "1.0.3", "mrclay/minify": "^2.2", "friendsofsymfony/rest-bundle": "^1.7", - "jms/serializer-bundle": "^1.1" + "jms/serializer-bundle": "^1.1", + "firebase/php-jwt": "^5.0" }, "require-dev": { "phpunit/phpunit": "^5.2.12", diff --git a/composer.lock b/composer.lock index 55f8a47d..23f5d3d2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "c9c3232f4906f7a6ee4e686702ce23ed", + "content-hash": "ced824e325238b6870ba894a1f0a6923", "packages": [ { "name": "composer/ca-bundle", @@ -944,6 +944,52 @@ ], "time": "2018-02-23T01:58:20+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": " 4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "time": "2017-06-27T22:17:23+00:00" + }, { "name": "friendsofsymfony/rest-bundle", "version": "1.7.7", @@ -1501,16 +1547,16 @@ }, { "name": "jms/serializer", - "version": "1.12.1", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "93d6e03fcb71d45854cc44b5a84d645c02c5d763" + "reference": "00863e1d55b411cc33ad3e1de09a4c8d3aae793c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/93d6e03fcb71d45854cc44b5a84d645c02c5d763", - "reference": "93d6e03fcb71d45854cc44b5a84d645c02c5d763", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/00863e1d55b411cc33ad3e1de09a4c8d3aae793c", + "reference": "00863e1d55b411cc33ad3e1de09a4c8d3aae793c", "shasum": "" }, "require": { @@ -1550,7 +1596,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.11-dev" + "dev-1.x": "1.13-dev" } }, "autoload": { @@ -1581,7 +1627,7 @@ "serialization", "xml" ], - "time": "2018-06-01T12:10:12+00:00" + "time": "2018-07-25T13:58:54+00:00" }, { "name": "jms/serializer-bundle", @@ -3165,16 +3211,16 @@ }, { "name": "swiftmailer/swiftmailer", - "version": "v5.4.9", + "version": "v5.4.10", "source": { "type": "git", "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "7ffc1ea296ed14bf8260b6ef11b80208dbadba91" + "reference": "dd71cc1638ed7aebbb33f2e2b0edd2cf6ea73d97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/7ffc1ea296ed14bf8260b6ef11b80208dbadba91", - "reference": "7ffc1ea296ed14bf8260b6ef11b80208dbadba91", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/dd71cc1638ed7aebbb33f2e2b0edd2cf6ea73d97", + "reference": "dd71cc1638ed7aebbb33f2e2b0edd2cf6ea73d97", "shasum": "" }, "require": { @@ -3215,7 +3261,7 @@ "mail", "mailer" ], - "time": "2018-01-23T07:37:21+00:00" + "time": "2018-07-27T08:58:59+00:00" }, { "name": "symfony/monolog-bundle", @@ -3811,12 +3857,12 @@ "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "03542492e99f7012ac633d09712e57ab84a565b0" + "reference": "7a3d825969fd7f6ccaa8fb737361baa715ffb71c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/03542492e99f7012ac633d09712e57ab84a565b0", - "reference": "03542492e99f7012ac633d09712e57ab84a565b0", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/7a3d825969fd7f6ccaa8fb737361baa715ffb71c", + "reference": "7a3d825969fd7f6ccaa8fb737361baa715ffb71c", "shasum": "" }, "require": { @@ -3869,7 +3915,7 @@ "keywords": [ "templating" ], - "time": "2018-07-20T13:30:04+00:00" + "time": "2018-07-25T13:16:22+00:00" }, { "name": "willdurand/jsonp-callback-validator", diff --git a/src/Casebox/CoreBundle/Service/Auth/CaseboxAuth.php b/src/Casebox/CoreBundle/Service/Auth/CaseboxAuth.php index 7b9564d4..4c3b2267 100755 --- a/src/Casebox/CoreBundle/Service/Auth/CaseboxAuth.php +++ b/src/Casebox/CoreBundle/Service/Auth/CaseboxAuth.php @@ -173,16 +173,14 @@ public function verifyUserPassword($username, $password) public function logout() { $user = $this->getEm()->getRepository('CaseboxCoreBundle:UsersGroups')->findUserByUsername(User::getUsername()); + $anonToken = new AnonymousToken('theTokensKey', 'anon.', []); + $this->getSecurityContext()->setToken($anonToken); + $this->getSession()->invalidate(); if (!$user instanceof UsersGroupsEntity) { return false; } $user->setLastLogout(time()); $this->getEm()->flush(); - - - $anonToken = new AnonymousToken('theTokensKey', 'anon.', []); - $this->getSecurityContext()->setToken($anonToken); - $this->getSession()->invalidate(); return true; } diff --git a/src/Casebox/CoreBundle/Service/System.php b/src/Casebox/CoreBundle/Service/System.php index 87c5c356..eab3763b 100755 --- a/src/Casebox/CoreBundle/Service/System.php +++ b/src/Casebox/CoreBundle/Service/System.php @@ -66,8 +66,11 @@ public function bootstrap(Container $container, Request $request = null) // Process user locale $user = $session->get('user'); - $language = (!empty($user['language'])) ? $user['language'] : $request->getLocale(); - $request->setLocale($language); + if(is_array($user)) + { + $language = (!empty($user['language'])) ? $user['language'] : $request->getLocale(); + $request->setLocale($language); + } } } diff --git a/src/Casebox/CoreBundle/Service/User.php b/src/Casebox/CoreBundle/Service/User.php index 4b06d403..1f130c1e 100755 --- a/src/Casebox/CoreBundle/Service/User.php +++ b/src/Casebox/CoreBundle/Service/User.php @@ -1090,7 +1090,7 @@ public static function getUsername($idOrData = false) $data = is_numeric($idOrData) ? static::getPreferences($idOrData) : $idOrData; - $rez = empty($data['name']) ? '' : $data['name']; + $rez = is_array($data)? (empty($data['name']) ? '' : $data['name']):''; return $rez; } diff --git a/src/Casebox/RestBundle/Controller/RestApiController.php b/src/Casebox/RestBundle/Controller/RestApiController.php index e7f955f5..9bfeffc3 100644 --- a/src/Casebox/RestBundle/Controller/RestApiController.php +++ b/src/Casebox/RestBundle/Controller/RestApiController.php @@ -8,6 +8,10 @@ use FOS\RestBundle\Controller\Annotations\View; use FOS\RestBundle\Controller\FOSRestController; use FOS\RestBundle\Request\ParamFetcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Casebox\CoreBundle\Service\Config; +use Casebox\CoreBundle\Service\BrowserView; /** * Class RestApiController @@ -34,13 +38,36 @@ class RestApiController extends FOSRestController * * @return View */ - public function browserAction(ParamFetcher $fetcher) + public function browserAction(Request $request, ParamFetcher $fetcher) { $action = $fetcher->get('action'); $data = $fetcher->get('data'); - - $result = null; - + + $configService = $this->get('casebox_core.service.config'); + + if(!($this->validateUser($request))) + { + return new Response(null,401); + } + + if ($action == 'saveFile') + { + $file['error'] = UPLOAD_ERR_OK; + $file['tmp_name'] = tempnam($configService->get('incomming_files_dir'), 'cbup'); + $file['dir'] = '/'; + + file_put_contents($file['tmp_name'],base64_decode($data['file'])); + + $file['name'] = urldecode($data['fileName']); + $file['pid'] = urldecode($data['pid']); + $file['type'] = 'image/png'; + $file['size'] = filesize($file['tmp_name']); + $file['md5'] = md5_file($file['tmp_name']); + + $_FILES = ['file' => $file]; + } + $browser = new \Casebox\CoreBundle\Service\Browser(); + $result = $browser->{$action}($data); return $this->view()->setData($result); } @@ -74,13 +101,13 @@ public function browserActionsAction(ParamFetcher $fetcher) * * @return View */ - public function browserTreeAction(ParamFetcher $fetcher) + public function browserTreeAction(Request $request, ParamFetcher $fetcher) { $action = $fetcher->get('action'); $data = $fetcher->get('data'); $result = null; - + return $this->view()->setData($result); } @@ -92,13 +119,20 @@ public function browserTreeAction(ParamFetcher $fetcher) * * @return View */ - public function browserViewAction(ParamFetcher $fetcher) + public function browserViewAction(Request $request, ParamFetcher $fetcher) { $action = $fetcher->get('action'); $data = $fetcher->get('data'); - - $result = null; - + + if(!($this->validateUser($request))) + { + return new Response(null,401); + } + + $sr = new \Casebox\CoreBundle\Service\BrowserView(); + $results = $sr->{$action}($data); + $result = $results; + return $this->view()->setData($result); } @@ -132,7 +166,7 @@ public function favoritesAction(ParamFetcher $fetcher) * * @return View */ - public function filesAction(ParamFetcher $fetcher) + public function filesAction(Request $request, ParamFetcher $fetcher) { $action = $fetcher->get('action'); $data = $fetcher->get('data'); @@ -365,10 +399,16 @@ public function userLoginAction(ParamFetcher $fetcher) { $username = $fetcher->get('username'); $password = $fetcher->get('password'); - - $result = null; - - return $this->view()->setData($result); + $restService = $this->get('casebox_rest.service.rest_api_service'); + + if (!empty($username) && !empty($password)) + { + $key = $restService->authenticate($username,$password); + } + + $result = array('jwt' => $key); + + return $this->view()->setData($result); } /** @@ -379,6 +419,7 @@ public function userLoginAction(ParamFetcher $fetcher) */ public function userLogoutAction(ParamFetcher $fetcher) { + $this->get('casebox_core.service_auth.authentication')->logout(); $result = null; return $this->view()->setData($result); @@ -441,4 +482,28 @@ public function usersGroupsAction(ParamFetcher $fetcher) return $this->view()->setData($result); } + + private function validateUser(Request $request) + { + $valid = false; + $restService = $this->get('casebox_rest.service.rest_api_service'); + $authHeader = $request->headers->get('Authorization'); + if ($authHeader) { + $user = $restService->validateUserJWT($authHeader); + if (is_array($user)) + { + $session = $request->getSession(); + $session->set('user', $user); + $session->save(); + $valid = true; + } + else { + $this->get('casebox_core.service_auth.authentication')->logout(); + } + } else + { + $this->get('casebox_core.service_auth.authentication')->logout(); + } + return $valid; + } } diff --git a/src/Casebox/RestBundle/Service/RestApiService.php b/src/Casebox/RestBundle/Service/RestApiService.php index ebbbfa63..e7b5f0b7 100644 --- a/src/Casebox/RestBundle/Service/RestApiService.php +++ b/src/Casebox/RestBundle/Service/RestApiService.php @@ -4,11 +4,88 @@ use FOS\RestBundle\View\View; use Symfony\Component\HttpFoundation\Response; +use Casebox\CoreBundle\Service\Cache; +use Firebase\JWT\JWT; +use Casebox\CoreBundle\Entity\UsersGroups; /** * Class RestApiService */ class RestApiService { - // code... + + /** + * @param string $username + * @param string $password + * + * @return bool + * @throws \Exception + */ public function authenticate($username,$password) + { + $configService = Cache::get('symfony.container')->get('casebox_core.service.config'); + $loginService = Cache::get('symfony.container')->get('casebox_core.service_auth.authentication'); + + $user = $loginService->authenticate($username, $password); + + if ($user instanceof UsersGroups) { + $tokenId = base64_encode(random_bytes(32)); + $issuedAt = time(); + $notBefore = $issuedAt; + $expire = $notBefore + $configService->get('jwt_expiration_seconds'); // Adding 60 seconds + $serverName = $configService->getProjectName(); // Retrieve the server name from config file + + /* + * Create the token as an array + */ + $data = [ + 'iat' => $issuedAt, // Issued at: time when the token was generated + 'jti' => $tokenId, // Json Token Id: an unique identifier for the token + 'iss' => $serverName, // Issuer + 'nbf' => $notBefore, // Not before + 'exp' => $expire, // Expire + 'data' => [ // Data related to the signer user + 'userId' => $user->getId(), // userid from the users table + 'userName' => $user->getUsername(), // User name + 'displayName' => $user->getFirstName(), // User display name + ] + ]; + + $secretKey = base64_decode($configService->get('jwt_key')); + $jwt = JWT::encode( + $data, //Data to be encoded in the JWT + $secretKey, // The signing key + $configService->get('jwt_algorithm') // Algorithm used to sign the token, see https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3 + ); + } + + return $jwt; + } + /** + * @param string $jwtKey + * + * @return bool + * @throws \Exception + */ + public function validateUserJWT($jwtKey) + { + $configService = Cache::get('symfony.container')->get('casebox_core.service.config'); + $user = null; + try { + $decoded = JWT::decode($jwtKey, base64_decode($configService->get('jwt_key')), array($configService->get('jwt_algorithm'))); + $user = [ + 'id' => $decoded->data->userId, + 'name' => $decoded->data->userName, + 'first_name' => $decoded->data->displayName, + 'last_name' => $decoded->data->displayName, + ]; + //$user = $this->getEm()->getRepository('CaseboxCoreBundle:UsersGroups')->findUserByUsername($decoded->data->userName); + } catch (\Exception $e) { + /* + * the token was not able to be decoded. + * this is likely because the signature was not able to be verified (tampered token) + */ + return null; + } + return $user; + } }