├── src ├── Events │ ├── .gitkeep │ ├── UserIsLoggingOut.php │ ├── UserHasResetPassword.php │ ├── UserIsLoggingIn.php │ ├── UserIsRegistering.php │ ├── UserHasRequestedPasswordReset.php │ ├── UserHasLoggedIn.php │ └── UserHasRegistered.php ├── Exceptions │ ├── .gitkeep │ ├── SocialLoginException.php │ ├── IdSiteException.php │ └── ActionAbortedException.php ├── Http │ ├── Helpers │ │ ├── IdSiteModel.php │ │ ├── PasswordPolicies.php │ │ ├── IdSiteRequest.php │ │ └── IdSiteSessionHelper.php │ ├── Traits │ │ ├── AuthenticatesUser.php │ │ └── Cookies.php │ ├── socialRoutes.php │ ├── Controllers │ │ ├── ForgotPasswordController.php │ │ ├── VerifyEmailController.php │ │ ├── ChangePasswordController.php │ │ ├── MeController.php │ │ ├── OauthController.php │ │ ├── SocialCallbackController.php │ │ ├── LoginController.php │ │ └── RegisterController.php │ ├── Middleware │ │ ├── Produces.php │ │ ├── RedirectIfAuthenticated.php │ │ └── Authenticate.php │ └── routes.php ├── Commands │ └── StormpathConfigCommand.php ├── Support │ ├── OauthResponse.php │ ├── Oauth.php │ └── StormpathServiceProvider.php └── config │ └── stormpath.yaml ├── id_rsa.enc └── composer.json /src/Events/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Exceptions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /id_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/stormpath-lumen/HEAD/id_rsa.enc -------------------------------------------------------------------------------- /src/Events/UserIsLoggingOut.php: -------------------------------------------------------------------------------- 1 | data = $data; 23 | } 24 | 25 | /** 26 | * Get the form data provided with this event 27 | * 28 | * @return array The form data 29 | */ 30 | public function getData() 31 | { 32 | return $this->data; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Events/UserIsRegistering.php: -------------------------------------------------------------------------------- 1 | data = $data; 23 | } 24 | 25 | /** 26 | * Get the form data provided with this event 27 | * 28 | * @return array The form data 29 | */ 30 | public function getData() 31 | { 32 | return $this->data; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Events/UserHasRequestedPasswordReset.php: -------------------------------------------------------------------------------- 1 | data = $data; 23 | } 24 | 25 | /** 26 | * Get the form data provided with this event 27 | * 28 | * @return array The form data 29 | */ 30 | public function getData() 31 | { 32 | return $this->data; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Exceptions/SocialLoginException.php: -------------------------------------------------------------------------------- 1 | account = $account; 24 | } 25 | 26 | /** 27 | * Get the account associated with this event 28 | * 29 | * @return Account The account associated with this event 30 | */ 31 | public function getAccount() 32 | { 33 | return $this->account; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/UserHasRegistered.php: -------------------------------------------------------------------------------- 1 | account = $account; 24 | } 25 | 26 | /** 27 | * Get the account associated with this event 28 | * 29 | * @return Account The account associated with this event 30 | */ 31 | public function getAccount() 32 | { 33 | return $this->account; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exceptions/ActionAbortedException.php: -------------------------------------------------------------------------------- 1 | authenticate($passwordGrant); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Http/Helpers/IdSiteRequest.php: -------------------------------------------------------------------------------- 1 | setProperty('token', $token); 26 | return $this; 27 | } 28 | 29 | public function setGrantType($type) 30 | { 31 | $this->setProperty('grant_type', $type); 32 | return $this; 33 | } 34 | 35 | public function getStormpathToken() 36 | { 37 | return $this->getProperty('token'); 38 | } 39 | 40 | public function getGrantType() 41 | { 42 | return $this->getProperty('grant_type'); 43 | } 44 | } -------------------------------------------------------------------------------- /src/Commands/StormpathConfigCommand.php: -------------------------------------------------------------------------------- 1 | info('Stormpath Configuration has been moved to ' . $path); 51 | } 52 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stormpath/lumen", 3 | "description": "Build simple, secure web applications with Stormpath and Laravel!", 4 | "license": "Apache-2.0", 5 | "type": "laravel-package", 6 | "keywords": [ 7 | "users", 8 | "laravel", 9 | "accounts", 10 | "stormpath", 11 | "authorization", 12 | "authentication" 13 | ], 14 | "authors": [ 15 | { 16 | "name": "Brian Retterer", 17 | "email": "brian@stormpath.com", 18 | "homepage": "https://stormpath.com", 19 | "role": "PHP Developer Evangelist" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=5.5", 24 | "stormpath/sdk": "~1.14", 25 | "illuminate/support": "~5.2", 26 | "illuminate/http": "~5.2", 27 | "illuminate/routing": "~5.2", 28 | "illuminate/validation": "~5.2", 29 | "illuminate/cookie": "~5.2", 30 | "illuminate/console": "~5.2", 31 | "bretterer/iso_duration_converter": "^0.1.0" 32 | }, 33 | "require-dev": { 34 | "phpunit/phpunit": "~4.0", 35 | "mockery/mockery": "~0.9", 36 | "laravel/lumen-framework": "~5.2" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Stormpath\\Lumen\\": "src/" 41 | } 42 | }, 43 | "support": { 44 | "source": "https://github.com/stormpath/stormpath-laravel", 45 | "email": "support@stormpath.com", 46 | "irc": "irc://irc.freenode.org/stormpath", 47 | "issues": "https://github.com/stormpath/stormpath-laravel/issues", 48 | "docs": "http://docs.stormpath.com/php/laravel/latest/" 49 | }, 50 | "replace": { 51 | "stormpath/laravel-auth": "self.version" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Support/OauthResponse.php: -------------------------------------------------------------------------------- 1 | getProperty('access_token'); 33 | } 34 | 35 | /** 36 | * Gets the tokenType property 37 | * 38 | * @return string 39 | */ 40 | public function getTokenType() 41 | { 42 | return $this->getProperty('token_type'); 43 | } 44 | 45 | /** 46 | * Gets the expiresIn property 47 | * 48 | * @return integer 49 | */ 50 | public function getExpiresIn() 51 | { 52 | return $this->getProperty('expires_in'); 53 | } 54 | 55 | /** 56 | * Gets the accessTokenHref property 57 | * 58 | * @return string 59 | */ 60 | public function getAccessTokenHref() 61 | { 62 | return $this->getProperty('stormpath_access_token_href'); 63 | } 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | } -------------------------------------------------------------------------------- /src/Http/Helpers/IdSiteSessionHelper.php: -------------------------------------------------------------------------------- 1 | $account->href, 33 | 'iat' => time()-1, 34 | 'status' => 'AUTHENTICATED', 35 | 'iss' => $application->href, 36 | 'aud' => config('stormpath.client.apiKey.id') 37 | ], config('stormpath.client.apiKey.secret'), 'HS256'); 38 | 39 | $idSiteRequest = new IdSiteRequest(); 40 | $idSiteRequest->stormpathToken = $jwt; 41 | $idSiteRequest->grantType = 'stormpath_token'; 42 | 43 | return app('stormpath.client')->getDataStore()->create($application->href . '/oauth/token', $idSiteRequest, Stormpath::ACCESS_TOKEN); 44 | 45 | 46 | } catch (\Exception $e) { 47 | throw new SocialLoginException($e->getMessage()); 48 | } 49 | 50 | } 51 | 52 | 53 | 54 | } -------------------------------------------------------------------------------- /src/Http/socialRoutes.php: -------------------------------------------------------------------------------- 1 | app->get(config('stormpath.web.social.facebook.uri'), ['as' => 'stormpath.callbacks.facebook', 'uses' => 'Stormpath\Lumen\Http\Controllers\SocialCallbackController@facebook']); 25 | } 26 | 27 | if (config('stormpath.web.social.google.enabled')) { 28 | $this->app->get(config('stormpath.web.social.google.uri'), ['as' => 'stormpath.callbacks.google', 'uses' => 'Stormpath\Lumen\Http\Controllers\SocialCallbackController@google']); 29 | } 30 | 31 | if (config('stormpath.web.social.github.enabled')) { 32 | $this->app->get(config('stormpath.web.social.github.uri'), ['as' => 'stormpath.callbacks.github', 'uses' => 'Stormpath\Lumen\Http\Controllers\SocialCallbackController@github']); 33 | } 34 | 35 | if (config('stormpath.web.social.linkedin.enabled')) { 36 | $this->app->get(config('stormpath.web.social.linkedin.uri'), ['as' => 'stormpath.callbacks.linkedin', 'uses' => 'Stormpath\Lumen\Http\Controllers\SocialCallbackController@linkedin']); 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/Http/Controllers/ForgotPasswordController.php: -------------------------------------------------------------------------------- 1 | request = $request; 38 | } 39 | 40 | public function postForgotPassword() 41 | { 42 | $token = urldecode($this->request->input('spToken')); 43 | 44 | $validator = $this->changePasswordValidator(); 45 | 46 | if($validator->fails()) { 47 | return $this->respondWithError('Validation Failed', 400, ['validatonErrors' => $validator->errors()]); 48 | } 49 | 50 | $application = app('stormpath.application'); 51 | 52 | try { 53 | $application->resetPassword($token, $newPassword); 54 | 55 | return $this->respondOk(); 56 | 57 | } catch (\Stormpath\Resource\ResourceError $re) { 58 | return $this->respondWithError($re->getMessage(), $re->getStatus()); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Support/Oauth.php: -------------------------------------------------------------------------------- 1 | setProperty('client_id', $clientId); 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * Gets the clientId property 39 | * 40 | * @return string 41 | */ 42 | public function getClientId() 43 | { 44 | return $this->getProperty('client_id'); 45 | } 46 | 47 | 48 | 49 | /** 50 | * Sets the clientSecret property 51 | * 52 | * @param string $clientSecret The clientSecret of the object 53 | * @return self 54 | */ 55 | public function setClientSecret($clientSecret) 56 | { 57 | $this->setProperty('client_secret', $clientSecret); 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Gets the clientSecret property 64 | * 65 | * @return string 66 | */ 67 | public function getClientSecret() 68 | { 69 | return $this->getProperty('client_secret'); 70 | } 71 | 72 | 73 | /** 74 | * Sets the grantType property 75 | * 76 | * @param string $grantType The grantType of the object 77 | * @return self 78 | */ 79 | public function setGrantType($grantType) 80 | { 81 | $this->setProperty('grant_type', $grantType); 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Gets the grantType property 88 | * 89 | * @return string 90 | */ 91 | public function getGrantType() 92 | { 93 | return $this->getProperty('grant_type'); 94 | } 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | } -------------------------------------------------------------------------------- /src/Http/Controllers/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | request = $request; 38 | } 39 | 40 | public function getVerifyEmail() 41 | { 42 | $token = urldecode($this->request->input('spToken')); 43 | if(null === $token || '' == $token) { 44 | return $this->respondWithError('sptoken parameter not provided.', 400); 45 | } 46 | 47 | $client = app('stormpath.client'); 48 | 49 | try { 50 | $client->verifyEmailToken($token); 51 | 52 | return response()->json(); 53 | 54 | } catch (\Stormpath\Resource\ResourceError $re) { 55 | return $this->respondWithError('Could not verify your email. Please request a new token.', $re->getStatus()); 56 | } 57 | } 58 | 59 | public function postVerifyEmail() 60 | { 61 | $email = $this->request->input('email'); 62 | if(null === $email || '' == $email) { 63 | return $this->respondWithError('email parameter not provided.', 400); 64 | } 65 | 66 | $application = app('stormpath.application'); 67 | 68 | try { 69 | $application->sendVerificationEmail($email); 70 | } catch (\Stormpath\Resource\ResourceError $re) {} 71 | 72 | return response()->json(); 73 | } 74 | 75 | private function respondWithError($message, $statusCode = 400, $extra = []) 76 | { 77 | $error = [ 78 | 'errors' => [ 79 | 'message' => $message 80 | ] 81 | ]; 82 | 83 | if(!empty($extra)) { 84 | $error['errors'] = array_merge($error['errors'], $extra); 85 | } 86 | return response()->json($error, $statusCode); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Http/Traits/Cookies.php: -------------------------------------------------------------------------------- 1 | makeAccessTokenCookie($accessToken); 29 | 30 | // $cookieJar->queue($cookie); 31 | } 32 | 33 | public function makeAccessTokenCookie($accessToken) 34 | { 35 | return new Cookie( 36 | config('stormpath.web.accessTokenCookie.name'), 37 | $accessToken, 38 | time() + $this->getExpiresTime('accessToken') / 60, 39 | config('stormpath.web.accessTokenCookie.path'), 40 | config('stormpath.web.accessTokenCookie.domain'), 41 | config('stormpath.web.accessTokenCookie.secure'), 42 | config('stormpath.web.accessTokenCookie.httpOnly') 43 | ); 44 | } 45 | 46 | public function queueRefreshToken($refreshToken) 47 | { 48 | // $cookieJar = app('cookie'); 49 | 50 | return $this->makeRefreshTokenCookie($refreshToken); 51 | 52 | // $cookieJar->queue($cookie); 53 | } 54 | 55 | public function makeRefreshTokenCookie($refreshToken) 56 | { 57 | return new Cookie( 58 | config('stormpath.web.refreshTokenCookie.name'), 59 | $refreshToken, 60 | time() + $this->getExpiresTime('refreshToken') / 60, 61 | config('stormpath.web.refreshTokenCookie.path'), 62 | config('stormpath.web.refreshTokenCookie.domain'), 63 | config('stormpath.web.refreshTokenCookie.secure'), 64 | config('stormpath.web.refreshTokenCookie.httpOnly') 65 | ); 66 | } 67 | 68 | private function getExpiresTime($type = 'accessToken') 69 | { 70 | $application = app('stormpath.application'); 71 | 72 | $policy = $application->oauthPolicy; 73 | $methodName = 'get' . ucfirst($type) . 'Ttl'; 74 | 75 | $policy->setOptions([]); 76 | 77 | $time = $policy->{$methodName}(['expand'=>'tokenEndpoint']); 78 | 79 | $converter = new \Bretterer\IsoDurationConverter\DurationParser(); 80 | 81 | $seconds = $converter->parse($time); 82 | 83 | return $seconds; 84 | 85 | } 86 | } -------------------------------------------------------------------------------- /src/Http/Middleware/Produces.php: -------------------------------------------------------------------------------- 1 | produces = config('stormpath.web.produces'); 49 | $acceptHeader = explode(',',$request->header('Accept')); 50 | $approvedProduces = array_intersect($this->systemProduces, $this->produces); 51 | 52 | if(!$this->hasApprovedProduces($approvedProduces)) { 53 | return $this->respondNotAcceptable('The system does not know how to respond to any accept headers defined.'); 54 | } 55 | 56 | if(in_array('*/*', $acceptHeader)) { 57 | $request->headers->remove('Accept'); 58 | $request->headers->set('Accept', $this->produces[0]); 59 | return $next($request); 60 | } 61 | 62 | if(!$this->acceptHeaderIsApproved($acceptHeader, $approvedProduces)) { 63 | return $this->respondNotAcceptable('Accept Header is not allowed.'); 64 | } 65 | 66 | return $next($request); 67 | } 68 | 69 | private function respondNotAcceptable($message) 70 | { 71 | return response($message, 406); 72 | } 73 | 74 | /** 75 | * @param $approvedProduces 76 | * @return bool 77 | */ 78 | private function hasApprovedProduces($approvedProduces) 79 | { 80 | return !! count($approvedProduces); 81 | } 82 | 83 | /** 84 | * @param $acceptHeader 85 | * @param $approvedProduces 86 | * @return int 87 | */ 88 | private function acceptHeaderIsApproved($acceptHeader, $approvedProduces) 89 | { 90 | return !! count(array_intersect((array)$acceptHeader, $approvedProduces)); 91 | } 92 | 93 | 94 | } -------------------------------------------------------------------------------- /src/Http/Controllers/ChangePasswordController.php: -------------------------------------------------------------------------------- 1 | request = $request; 49 | $this->validator = app('validator'); 50 | } 51 | 52 | public function postChangePassword() 53 | { 54 | $newPassword = $this->request->input('password'); 55 | $token = urldecode($this->request->input('spToken')); 56 | 57 | $validator = $this->changePasswordValidator(); 58 | 59 | if($validator->fails()) { 60 | return $this->respondWithError('Validation Failed', 400, ['validatonErrors' => $validator->errors()]); 61 | } 62 | 63 | $application = app('stormpath.application'); 64 | 65 | try { 66 | $application->resetPassword($token, $newPassword); 67 | 68 | return $this->respondOk(); 69 | 70 | } catch (\Stormpath\Resource\ResourceError $re) { 71 | return $this->respondWithError($re->getMessage(), $re->getStatus()); 72 | } 73 | } 74 | 75 | private function changePasswordValidator() 76 | { 77 | $validator = $this->validator->make( 78 | $this->request->all(), 79 | [ 80 | 'password' => 'required' 81 | ], 82 | [ 83 | 'password.required' => 'Password is required.' 84 | ] 85 | ); 86 | 87 | 88 | return $validator; 89 | } 90 | 91 | private function respondOk() 92 | { 93 | return response()->json(); 94 | } 95 | 96 | private function respondWithError($message, $statusCode = 400, $extra = []) 97 | { 98 | $error = [ 99 | 'errors' => [ 100 | 'message' => $message 101 | ] 102 | ]; 103 | 104 | if(!empty($extra)) { 105 | $error['errors'] = array_merge($error['errors'], $extra); 106 | } 107 | return response()->json($error, $statusCode); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/Http/Controllers/MeController.php: -------------------------------------------------------------------------------- 1 | cookies->get(config('stormpath.web.accessTokenCookie.name')); 30 | 31 | if($request->headers->has('Authorization')) { 32 | $token = explode(' ', $request->headers->get('Authorization')); 33 | $accessToken = end($token); 34 | } 35 | 36 | try { 37 | $account = $this->getAccountFromAccessToken($accessToken); 38 | return $this->respondWithAccount($account); 39 | } catch (\Exception $e) { 40 | return response('', 401); 41 | } 42 | } 43 | 44 | private function respondWithAccount(Account $account) 45 | { 46 | $properties = ['account'=>[]]; 47 | $config = config('stormpath.web.me.expand'); 48 | $whiteListResources = []; 49 | foreach($config as $item=>$value) { 50 | if($value == true) { 51 | $whiteListResources[] = $item; 52 | } 53 | } 54 | 55 | $propNames = $account->getPropertyNames(); 56 | foreach($propNames as $prop) { 57 | $property = $this->getPropertyValue($account, $prop); 58 | 59 | if(is_object($property) && !in_array($prop, $whiteListResources)) { 60 | continue; 61 | } 62 | 63 | $properties['account'][$prop] = $property; 64 | } 65 | return response()->json($properties); 66 | } 67 | 68 | private function getPropertyValue($account, $prop) 69 | { 70 | $value = null; 71 | try { 72 | $value = $account->getProperty($prop); 73 | } catch (\Exception $e) { 74 | return null; 75 | } 76 | 77 | return $value; 78 | 79 | } 80 | 81 | private function getAccountFromAccessToken($accessToken) 82 | { 83 | \Firebase\JWT\JWT::$leeway = 10; 84 | 85 | $jwt = \Firebase\JWT\JWT::decode($accessToken, config('stormpath.client.apiKey.secret'), ['HS256']); 86 | 87 | $expandsArray = []; 88 | $expands = config('stormpath.web.me.expand'); 89 | foreach($expands as $key=>$value) { 90 | if($value == false) continue; 91 | $expandsArray[] = $key; 92 | } 93 | $toExpand = []; 94 | if(count($expandsArray) > 0) { 95 | $toExpand = ['expand' => implode(',',$expandsArray)]; 96 | } 97 | 98 | $account = \Stormpath\Resource\Account::get($jwt->sub, $toExpand); 99 | return $account; 100 | } 101 | } -------------------------------------------------------------------------------- /src/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | cookieJar = $cookieJar; 31 | } 32 | 33 | /** 34 | * Handle an incoming request. 35 | * 36 | * @param \Illuminate\Http\Request $request 37 | * @param \Closure $next 38 | * @return mixed 39 | */ 40 | public function handle($request, Closure $next) 41 | { 42 | if ($this->isAuthenticated($request)) { 43 | return redirect()->intended('/'); 44 | } 45 | 46 | if($request->wantsJson()) { 47 | return response(null, 401); 48 | } 49 | 50 | $accessToken = $this->refreshCookie($request); 51 | 52 | if (null !== $accessToken) { 53 | return redirect()->intended('/'); 54 | } 55 | 56 | return $next($request); 57 | } 58 | 59 | 60 | public function isAuthenticated($request) 61 | { 62 | $cookie = $request->cookie(config('stormpath.web.accessTokenCookie.name')); 63 | 64 | if(null === $cookie) { 65 | return false; 66 | } 67 | 68 | if($cookie instanceof \Symfony\Component\HttpFoundation\Cookie) { 69 | $cookie = $cookie->getValue(); 70 | } 71 | 72 | try { 73 | (new \Stormpath\Oauth\VerifyAccessToken(app('stormpath.application')))->verify($cookie); 74 | return true; 75 | } catch (\Exception $re) { 76 | return false; 77 | } 78 | } 79 | 80 | private function refreshCookie($request) 81 | { 82 | try { 83 | $spApplication = app('stormpath.application'); 84 | } catch (\Exception $e) { 85 | return null; 86 | } 87 | 88 | $cookie = $request->cookie(config('stormpath.web.refreshTokenCookie.name')); 89 | if($cookie instanceof \Symfony\Component\HttpFoundation\Cookie) 90 | $cookie = $cookie->getValue(); 91 | 92 | try { 93 | $refreshGrant = new \Stormpath\Oauth\RefreshGrantRequest($cookie); 94 | $auth = new \Stormpath\Oauth\RefreshGrantAuthenticator($spApplication); 95 | $result = $auth->authenticate($refreshGrant); 96 | 97 | $this->setNewAccessToken($request, $result); 98 | 99 | return $result->getAccessTokenString(); 100 | 101 | } catch(\Stormpath\Resource\ResourceError $re) { 102 | return null; 103 | } 104 | } 105 | 106 | private function setNewAccessToken($request, $cookies) 107 | { 108 | $this->cookieJar->queue( 109 | cookie( 110 | config('stormpath.web.accessTokenCookie.name'), 111 | $cookies->getAccessTokenString(), 112 | $cookies->getExpiresIn(), 113 | config('stormpath.web.accessTokenCookie.path'), 114 | config('stormpath.web.accessTokenCookie.domain'), 115 | config('stormpath.web.accessTokenCookie.secure'), 116 | config('stormpath.web.accessTokenCookie.httpOnly') 117 | ) 118 | 119 | ); 120 | 121 | 122 | $request->cookies->add([config('stormpath.web.accessTokenCookie.name') => $cookies->getAccessTokenString() ]); 123 | 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | cookieJar = $cookieJar; 32 | } 33 | 34 | /** 35 | * Handle an incoming request to make sure a user is authenticated to allow them to view the route 36 | * 37 | * @param \Illuminate\Http\Request $request 38 | * @param \Closure $next 39 | * @return mixed 40 | */ 41 | public function handle(Request $request, Closure $next) 42 | { 43 | if ($this->isAuthenticated($request)) { 44 | return $next($request); 45 | } 46 | 47 | if ($this->refreshTokens($request)) { 48 | return $next($request); 49 | } 50 | 51 | return $this->responseUnauthenticated($request); 52 | 53 | } 54 | 55 | public function isAuthenticated(Request $request) 56 | { 57 | $token = $request->bearerToken(); 58 | 59 | if(null === $token) { 60 | $token = $request->cookie(config('stormpath.web.accessTokenCookie.name')); 61 | } 62 | 63 | if($token instanceof \Symfony\Component\HttpFoundation\Cookie) { 64 | $token = $token->getValue(); 65 | } 66 | 67 | try { 68 | if(config('stormpath.web.oauth2.password.validationStrategy') == 'local') { 69 | \Firebase\JWT\JWT::$leeway = 10; 70 | $expanded = \Firebase\JWT\JWT::decode($token, config('stormpath.client.apiKey.secret'), ['HS256']); 71 | if ($expanded->exp > time()) { 72 | return true; 73 | } 74 | return false; 75 | } 76 | 77 | (new \Stormpath\Oauth\VerifyAccessToken(app('stormpath.application')))->verify($token); 78 | return true; 79 | } catch (\Exception $re) { 80 | return false; 81 | } 82 | } 83 | 84 | public function refreshTokens(Request $request) { 85 | if ($request->wantsJson()) { 86 | return false; 87 | } 88 | 89 | try { 90 | $spApplication = app('stormpath.application'); 91 | } catch (\Exception $e) { 92 | return false; 93 | } 94 | 95 | $cookie = $request->cookie(config('stormpath.web.refreshTokenCookie.name')); 96 | if($cookie instanceof \Symfony\Component\HttpFoundation\Cookie) 97 | $cookie = $cookie->getValue(); 98 | 99 | try { 100 | $refreshGrant = new \Stormpath\Oauth\RefreshGrantRequest($cookie); 101 | $auth = new \Stormpath\Oauth\RefreshGrantAuthenticator($spApplication); 102 | $result = $auth->authenticate($refreshGrant); 103 | 104 | $this->setNewAccessToken($request, $result); 105 | 106 | return true; 107 | 108 | } catch(\Stormpath\Resource\ResourceError $re) { 109 | return false; 110 | } 111 | 112 | } 113 | 114 | private function responseUnauthenticated(Request $request) 115 | { 116 | if ($request->wantsJson()) { 117 | return response(null, 401); 118 | } 119 | 120 | return redirect()->route('stormpath.login'); 121 | } 122 | 123 | private function setNewAccessToken($request, $cookies) 124 | { 125 | $this->cookieJar->queue( 126 | cookie( 127 | config('stormpath.web.accessTokenCookie.name'), 128 | $cookies->getAccessTokenString(), 129 | $cookies->getExpiresIn(), 130 | config('stormpath.web.accessTokenCookie.path'), 131 | config('stormpath.web.accessTokenCookie.domain'), 132 | config('stormpath.web.accessTokenCookie.secure'), 133 | config('stormpath.web.accessTokenCookie.httpOnly') 134 | ) 135 | 136 | ); 137 | 138 | 139 | $request->cookies->add([config('stormpath.web.accessTokenCookie.name') => $cookies->getAccessTokenString() ]); 140 | 141 | } 142 | 143 | 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/Http/routes.php: -------------------------------------------------------------------------------- 1 | app->get(config('stormpath.web.login.uri'), ['as' => 'stormpath.login', 'uses' => 'Stormpath\Lumen\Http\Controllers\LoginController@getLogin']); 36 | $this->app->post(config('stormpath.web.login.uri'), ['as' => 'stormpath.login', 'uses' => 'Stormpath\Lumen\Http\Controllers\LoginController@postLogin']); 37 | } 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Logout Routes 42 | |-------------------------------------------------------------------------- 43 | */ 44 | if (config('stormpath.web.logout.enabled')) { 45 | $this->app->post(config('stormpath.web.logout.uri'), ['as' => 'stormpath.logout', 'uses' => 'Stormpath\Lumen\Http\Controllers\LoginController@getLogout']); 46 | } 47 | 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | Register Routes 51 | |-------------------------------------------------------------------------- 52 | */ 53 | if (config('stormpath.web.register.enabled')) { 54 | $this->app->get(config('stormpath.web.register.uri'), ['as' => 'stormpath.register', 'uses' => 'Stormpath\Lumen\Http\Controllers\RegisterController@getRegister']); 55 | $this->app->post(config('stormpath.web.register.uri'), ['as' => 'stormpath.register', 'uses' => 'Stormpath\Lumen\Http\Controllers\RegisterController@postRegister']); 56 | } 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | Verify Email Routes 61 | |-------------------------------------------------------------------------- 62 | */ 63 | if (config('stormpath.web.verifyEmail.enabled')) { 64 | $this->app->get(config('stormpath.web.verifyEmail.uri'), ['as' => 'stormpath.verifyEmail', 'uses' => 'Stormpath\Lumen\Http\Controllers\VerifyEmailController@getVerifyEmail']); 65 | $this->app->post(config('stormpath.web.verifyEmail.uri'), ['as' => 'stormpath.verifyEmail', 'uses' => 'Stormpath\Lumen\Http\Controllers\VerifyEmailController@postVerifyEmail']); 66 | } 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Forgot Password Routes 71 | |-------------------------------------------------------------------------- 72 | */ 73 | if (config('stormpath.web.forgotPassword.enabled')) { 74 | $this->app->post(config('stormpath.web.forgotPassword.uri'), ['as' => 'stormpath.forgotPassword', 'uses' => 'Stormpath\Lumen\Http\Controllers\ForgotPasswordController@postForgotPassword']); 75 | } 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Change Password Routes 80 | |-------------------------------------------------------------------------- 81 | */ 82 | if (config('stormpath.web.changePassword.enabled')) { 83 | $this->app->post(config('stormpath.web.changePassword.uri'), ['as' => 'stormpath.changePassword', 'uses' => 'Stormpath\Lumen\Http\Controllers\ChangePasswordController@postChangePassword']); 84 | } 85 | 86 | /* 87 | |-------------------------------------------------------------------------- 88 | | Oauth Routes 89 | |-------------------------------------------------------------------------- 90 | */ 91 | if (config('stormpath.web.oauth2.enabled')) { 92 | $this->app->post(config('stormpath.web.oauth2.uri'), ['as' => 'stormpath.oauth.token', 'uses' => 'Stormpath\Lumen\Http\Controllers\OauthController@getTokens']); 93 | } 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Me Routes 98 | |-------------------------------------------------------------------------- 99 | */ 100 | if (config('stormpath.web.me.enabled')) { 101 | $this->app->get(config('stormpath.web.me.uri'), ['as' => 'stormpath.me', 'uses' => 'Stormpath\Lumen\Http\Controllers\MeController@getMe']); 102 | } 103 | -------------------------------------------------------------------------------- /src/config/stormpath.yaml: -------------------------------------------------------------------------------- 1 | web: 2 | 3 | basePath: null 4 | 5 | oauth2: 6 | enabled: true 7 | uri: "/oauth/token" 8 | client_credentials: 9 | enabled: true 10 | accessToken: 11 | ttl: 3600 12 | password: 13 | enabled: true 14 | validationStrategy: "local" 15 | 16 | accessTokenCookie: 17 | name: "access_token" 18 | httpOnly: true 19 | 20 | # See cookie-authentication.md for explanation of 21 | # how `null` values behave for these properties. 22 | secure: null 23 | path: null 24 | domain: null 25 | 26 | refreshTokenCookie: 27 | name: "refresh_token" 28 | httpOnly: true 29 | 30 | # See cookie-authentication.md for explanation of 31 | # how `null` values behave for these properties. 32 | secure: null 33 | path: null 34 | domain: null 35 | 36 | # By default the Stormpath integration will respond to JSON and HTML 37 | # requests. If a requested type is not in this list, the Stormpath 38 | # integration should pass on the request, and allow the developer or base 39 | # framework to handle the response. 40 | # 41 | # If the request does not specify an Accept header, or the preferred content 42 | # type is */*, the Stormpath integration will respond with the first type in 43 | # this list. 44 | produces: 45 | - application/json 46 | 47 | register: 48 | enabled: true 49 | uri: "/register" 50 | nextUri: "/" 51 | # autoLogin is possible only if the email verification feature is disabled 52 | # on the default account store of the defined Stormpath 53 | # application. 54 | autoLogin: false 55 | form: 56 | fields: 57 | givenName: 58 | enabled: true 59 | label: "First Name" 60 | placeholder: "First Name" 61 | required: true 62 | type: "text" 63 | middleName: 64 | enabled: false 65 | label: "Middle Name" 66 | placeholder: "Middle Name" 67 | required: true 68 | type: "text" 69 | surname: 70 | enabled: true 71 | label: "Last Name" 72 | placeholder: "Last Name" 73 | required: true 74 | type: "text" 75 | username: 76 | enabled: false 77 | label: "Username" 78 | placeholder: "Username" 79 | required: true 80 | type: "text" 81 | email: 82 | enabled: true 83 | label: "Email" 84 | placeholder: "Email" 85 | required: true 86 | type: "email" 87 | password: 88 | enabled: true 89 | label: "Password" 90 | placeholder: "Password" 91 | required: true 92 | type: "password" 93 | confirmPassword: 94 | enabled: false 95 | label: "Confirm Password" 96 | placeholder: "Confirm Password" 97 | required: true 98 | type: "password" 99 | fieldOrder: 100 | - "username" 101 | - "givenName" 102 | - "middleName" 103 | - "surname" 104 | - "email" 105 | - "password" 106 | - "confirmPassword" 107 | 108 | # Unless verifyEmail.enabled is specifically set to false, the email 109 | # verification feature must be automatically enabled if the default account 110 | # store for the defined Stormpath application has the email verification 111 | # workflow enabled. 112 | verifyEmail: 113 | enabled: null 114 | uri: "/verify" 115 | nextUri: "/login" 116 | 117 | login: 118 | enabled: true 119 | uri: "/login" 120 | nextUri: "/" 121 | form: 122 | fields: 123 | login: 124 | enabled: true 125 | label: "Username or Email" 126 | placeholder: "Username or Email" 127 | required: true 128 | type: "text" 129 | password: 130 | enabled: true 131 | label: "Password" 132 | placeholder: "Password" 133 | required: true 134 | type: "password" 135 | fieldOrder: 136 | - "login" 137 | - "password" 138 | 139 | logout: 140 | enabled: true 141 | uri: "/logout" 142 | nextUri: "/" 143 | 144 | # Unless forgotPassword.enabled is explicitly set to false, this feature 145 | # will be automatically enabled if the default account store for the defined 146 | # Stormpath application has the password reset workflow enabled. 147 | forgotPassword: 148 | enabled: null 149 | uri: "/forgot" 150 | nextUri: "/login?status=forgot" 151 | 152 | # Unless changePassword.enabled is explicitly set to false, this feature 153 | # will be automatically enabled if the default account store for the defined 154 | # Stormpath application has the password reset workflow enabled. 155 | changePassword: 156 | enabled: null 157 | autoLogin: false 158 | uri: "/change" 159 | nextUri: "/login?status=reset" 160 | errorUri: "/forgot?status=invalid_sptoken" 161 | 162 | # Social login configuration. This defines the callback URIs for OAuth 163 | # flows, and the scope that is requested of each provider. Some providers 164 | # want space-separated scopes, some want comma-separated. As such, these 165 | # string values should be passed directly, as defined. 166 | # 167 | # These settings have no affect if the application does not have an account 168 | # store for the given provider. 169 | social: 170 | facebook: 171 | uri: "/callbacks/facebook" 172 | scope: "email" 173 | google: 174 | uri: "/callbacks/google" 175 | scope: "email profile" 176 | linkedin: 177 | uri: "/callbacks/linkedin" 178 | scope: "r_basicprofile r_emailaddress" 179 | 180 | # The /me route is for front-end applications, it returns a JSON object with 181 | # the current user object. The developer can opt-in to expanding account 182 | # resources on this enpdoint. 183 | me: 184 | enabled: true 185 | uri: "/me" 186 | expand: 187 | apiKeys: false 188 | applications: false 189 | customData: false 190 | directory: false 191 | groupMemberships: false 192 | groups: false 193 | providerData: false 194 | tenant: false 195 | 196 | -------------------------------------------------------------------------------- /src/Http/Controllers/OauthController.php: -------------------------------------------------------------------------------- 1 | getGrantType($request); 35 | 36 | switch($grantType) { 37 | case 'password' : 38 | return $this->doPasswordGrantType($request); 39 | // @codeCoverageIgnoreStart 40 | case 'client_credentials' : 41 | return $this->doClientCredentialsGrantType($request); 42 | // @codeCoverageIgnoreEnd 43 | case 'refresh_token' : 44 | return $this->doRefreshGrantType($request); 45 | default : 46 | return $this->respondUnsupportedGrantType(); 47 | } 48 | 49 | } 50 | 51 | /** @codeCoverageIgnore */ 52 | private function doClientCredentialsGrantType($request) 53 | { 54 | if(!config('stormpath.web.oauth2.client_credentials.enabled')) { 55 | return $this->respondUnsupportedGrantType(); 56 | } 57 | 58 | if(!$this->hasAuthorizationHeader($request)) { 59 | return $this->respondWithInvalidRequest('You must supply Basic Authorization Header'); 60 | } 61 | 62 | $token = base64_decode($this->basicToken($request)); 63 | 64 | if('' == $token) { 65 | return $this->respondWithInvalidRequest('The authorization header is in an invalid form'); 66 | } 67 | 68 | list($id, $secret) = explode(':',$token); 69 | 70 | 71 | $oauth = new Oauth(); 72 | $oauth->clientId = $id; 73 | $oauth->clientSecret = $secret; 74 | $oauth->grantType = 'client_credentials'; 75 | 76 | $response = app('stormpath.client')->getDataStore()->create(app('stormpath.application')->href . '/oauth/token', $oauth, \Stormpath\Lumen\Support\OauthResponse::class); 77 | 78 | return response()->json([ 79 | 'access_token' => $response->accessToken, 80 | 'token_type' => $response->tokenType, 81 | 'expires_in' => $response->expiresIn, 82 | 'stormpath_access_token_href' => $response->accessTokenHref 83 | ]); 84 | 85 | 86 | } 87 | 88 | private function doPasswordGrantType($request) 89 | { 90 | try { 91 | $passwordGrant = new \Stormpath\Oauth\PasswordGrantRequest($request->input('username'), $request->input('password')); 92 | $auth = new \Stormpath\Oauth\PasswordGrantAuthenticator(app('stormpath.application')); 93 | $result = $auth->authenticate($passwordGrant); 94 | return $this->respondWithAccessTokens($result); 95 | } catch (\Exception $e) { 96 | return $this->respondWithInvalidLogin($e); 97 | } 98 | } 99 | 100 | private function respondUnsupportedGrantType() 101 | { 102 | return response()->json([ 103 | 'message' => 'The authorization grant type is not supported by the authorization server.', 104 | 'error' => 'unsupported_grant_type' 105 | ], 400); 106 | } 107 | 108 | private function getGrantType($request) 109 | { 110 | return $request->input('grant_type'); 111 | } 112 | 113 | private function respondWithInvalidLogin($e) 114 | { 115 | return response()->json([ 116 | 'message' => $e->getMessage(), 117 | 'error' => 'invalid_grant' 118 | ], 400); 119 | } 120 | 121 | private function respondWithAccessTokens(OauthGrantAuthenticationResult $result) 122 | { 123 | return response()->json([ 124 | 'access_token' => $result->getAccessTokenString(), 125 | 'expires_in' => $result->getExpiresIn(), 126 | 'refresh_token' => $result->getRefreshTokenString(), 127 | 'token_type' => 'Bearer' 128 | ]); 129 | } 130 | 131 | private function doRefreshGrantType($request) 132 | { 133 | if(null === $request->input('refresh_token')) { 134 | return $this->respondWithInvalidRequest('The refresh_token parameter is required.'); 135 | } 136 | 137 | try { 138 | $refreshGrant = new \Stormpath\Oauth\RefreshGrantRequest($request->input('refresh_token')); 139 | 140 | $auth = new \Stormpath\Oauth\RefreshGrantAuthenticator(app('stormpath.application')); 141 | $result = $auth->authenticate($refreshGrant); 142 | return $this->respondWithAccessTokens($result); 143 | } catch (\Exception $e) { 144 | return $this->respondWithInvalidLogin($e); 145 | } 146 | 147 | } 148 | 149 | private function respondWithInvalidRequest($message = 'Invalid Request') 150 | { 151 | return response()->json([ 152 | 'message' => $message, 153 | 'error' => 'invalid_request' 154 | ], 400); 155 | } 156 | 157 | 158 | private function hasAuthorizationHeader(Request $request) 159 | { 160 | return null !== $request->header('Authorization'); 161 | } 162 | 163 | /** 164 | * Get the basic token from the request headers. 165 | * 166 | * @return string|null 167 | */ 168 | public function basicToken(Request $request) 169 | { 170 | $header = $request->header('Authorization', ''); 171 | 172 | if (Str::startsWith($header, 'Basic ')) { 173 | return Str::substr($header, 6); 174 | } 175 | } 176 | 177 | } -------------------------------------------------------------------------------- /src/Http/Controllers/SocialCallbackController.php: -------------------------------------------------------------------------------- 1 | application = $application; 44 | 45 | app('cache.store')->forget('stormpath.application'); 46 | 47 | if(null === $this->application) { 48 | $this->application = app('stormpath.application'); 49 | } 50 | 51 | } 52 | 53 | public function facebook(Request $request) 54 | { 55 | try { 56 | 57 | $providerAccountRequest = new FacebookProviderAccountRequest($this->buildProviderAccountRequestArray($request)); 58 | 59 | $account = $this->sendProviderAccountRequest($providerAccountRequest); 60 | 61 | return $this->respondWithAccount($account); 62 | 63 | } catch (\Stormpath\Resource\ResourceError $re) { 64 | return $this->respondWithError($re); 65 | } catch (SocialLoginException $e) { 66 | return $this->respondWithError($e); 67 | } 68 | 69 | } 70 | 71 | public function google(Request $request) 72 | { 73 | try { 74 | $providerAccountRequest = new \Stormpath\Provider\GoogleProviderAccountRequest($this->buildProviderAccountRequestArray($request)); 75 | 76 | $account = $this->sendProviderAccountRequest($providerAccountRequest); 77 | 78 | return $this->respondWithAccount($account); 79 | 80 | } catch (\Stormpath\Resource\ResourceError $re) { 81 | return $this->respondWithError($re); 82 | } catch (SocialLoginException $e) { 83 | return $this->respondWithError($e); 84 | } 85 | 86 | } 87 | 88 | public function github(Request $request) 89 | { 90 | try { 91 | 92 | $providerAccountRequest = new GithubProviderAccountRequest($this->buildProviderAccountRequestArray($request)); 93 | 94 | $account = $this->sendProviderAccountRequest($providerAccountRequest); 95 | 96 | return $this->respondWithAccount($account); 97 | 98 | } catch (\Stormpath\Resource\ResourceError $re) { 99 | return $this->respondWithError($re); 100 | } catch (SocialLoginException $e) { 101 | return $this->respondWithError($e); 102 | } 103 | 104 | } 105 | 106 | public function linkedin(Request $request) 107 | { 108 | try { 109 | 110 | $providerAccountRequest = new LinkedInProviderAccountRequest($this->buildProviderAccountRequestArray($request)); 111 | 112 | $account = $this->sendProviderAccountRequest($providerAccountRequest); 113 | 114 | return $this->respondWithAccount($account); 115 | 116 | } catch (\Stormpath\Resource\ResourceError $re) { 117 | return $this->respondWithError($re); 118 | } catch (SocialLoginException $e) { 119 | return $this->respondWithError($e); 120 | } 121 | 122 | } 123 | 124 | 125 | protected function sendProviderAccountRequest(ProviderAccountRequest $providerAccountRequest) 126 | { 127 | $result = $this->application->getAccount($providerAccountRequest); 128 | return $result->account; 129 | } 130 | 131 | protected function getCookies(Account $account) 132 | { 133 | $idSiteSession = new IdSiteSessionHelper(); 134 | $accessTokens = $idSiteSession->create($account); 135 | $tokens = []; 136 | $tokens['access'] = $this->makeAccessTokenCookie($accessTokens->getProperty('access_token')); 137 | $tokens['refresh'] = $this->makeRefreshTokenCookie($accessTokens->getProperty('refresh_token')); 138 | return $tokens; 139 | } 140 | 141 | private function respondWithAccount(Account $account) 142 | { 143 | $properties = ['account'=>[]]; 144 | $config = config('stormpath.web.me.expand'); 145 | $whiteListResources = []; 146 | foreach($config as $item=>$value) { 147 | if($value == true) { 148 | $whiteListResources[] = $item; 149 | } 150 | } 151 | 152 | $propNames = $account->getPropertyNames(); 153 | foreach($propNames as $prop) { 154 | $property = $this->getPropertyValue($account, $prop); 155 | 156 | if(is_object($property) && !in_array($prop, $whiteListResources)) { 157 | continue; 158 | } 159 | 160 | $properties['account'][$prop] = $property; 161 | } 162 | 163 | $tokens = $this->getCookies($account); 164 | 165 | return response()->json($properties)->cookie($tokens['access'])->cookie($tokens['refresh']); 166 | } 167 | 168 | private function getPropertyValue($account, $prop) 169 | { 170 | $value = null; 171 | try { 172 | $value = $account->getProperty($prop); 173 | } catch (\Exception $e) { 174 | return null; 175 | } 176 | 177 | return $value; 178 | 179 | } 180 | 181 | private function respondWithError($exception) 182 | { 183 | return response()->json([ 184 | 'message' => $exception->getMessage(), 185 | 'status' => $exception->getStatus() 186 | ], $exception->getStatus()); 187 | } 188 | 189 | private function buildProviderAccountRequestArray($request) 190 | { 191 | $array = []; 192 | 193 | if($request->has('code')) { 194 | $array['code'] = $request->get('code'); 195 | } 196 | 197 | if($request->has('access_token')) { 198 | $array['accessToken'] = $request->get('access_token'); 199 | } 200 | 201 | if($request->has('providerData')) { 202 | if(!empty($request->get('providerData')['accessToken'])) { 203 | $array['accessToken'] = $request->get('providerData')['accessToken']; 204 | } 205 | } 206 | 207 | return $array; 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /src/Http/Controllers/LoginController.php: -------------------------------------------------------------------------------- 1 | request = $request; 50 | $this->validator = app('validator'); 51 | } 52 | 53 | public function getLogin() 54 | { 55 | return $this->respondWithForm(); 56 | } 57 | 58 | public function postLogin() 59 | { 60 | 61 | if($this->isSocialLoginAttempt()) { 62 | return $this->doSocialLogin(); 63 | } 64 | 65 | $validator = $this->loginValidator(); 66 | 67 | if($validator->fails()) { 68 | return $this->respondWithValidationErrorForJson($validator); 69 | } 70 | 71 | try { 72 | 73 | $result = $this->authenticate($this->request->input('login'), $this->request->input('password')); 74 | $accessTokenCookie = $this->makeAccessTokenCookie($result->getAccessTokenString()); 75 | $refreshTokenCookie = $this->makeRefreshTokenCookie($result->getRefreshTokenString()); 76 | 77 | $account = $result->getAccessToken()->getAccount(); 78 | 79 | return $this->respondWithAccount($account, $accessTokenCookie, $refreshTokenCookie); 80 | 81 | } catch (\Stormpath\Resource\ResourceError $re) { 82 | return $this->respondWithError($re->getMessage(), $re->getStatus()); 83 | } 84 | } 85 | 86 | public function getLogout() 87 | { 88 | return response() 89 | ->json() 90 | ->cookie( 91 | new Cookie(config('stormpath.web.accessTokenCookie.name'), null, 0) 92 | ) 93 | ->cookie( 94 | new Cookie(config('stormpath.web.refreshTokenCookie.name'), null, 0) 95 | ); 96 | } 97 | 98 | private function loginValidator() 99 | { 100 | $validator = $this->validator->make( 101 | $this->request->all(), 102 | [ 103 | 'login' => 'required', 104 | 'password' => 'required' 105 | ], 106 | [ 107 | 'login.required' => 'Login is required.', 108 | 'password.required' => 'Password is required.' 109 | ] 110 | ); 111 | 112 | 113 | return $validator; 114 | } 115 | 116 | private function respondWithForm() 117 | { 118 | 119 | $data = [ 120 | 'form' => [ 121 | 'fields' => [ 122 | [ 123 | 'label' => 'Username or Email', 124 | 'name' => 'login', 125 | 'placeholder' => 'Username or Email', 126 | 'required' => true, 127 | 'type' => 'text' 128 | ], 129 | [ 130 | 'label' => 'Password', 131 | 'name' => 'password', 132 | 'placeholder' => 'Password', 133 | 'required' => true, 134 | 'type' => 'password' 135 | ] 136 | ] 137 | ], 138 | 'accountStores' => app('cache.store')->get('stormpath.accountStores'), 139 | 140 | ]; 141 | return response($data)->header('Content-Type', 'application/json'); 142 | 143 | } 144 | 145 | private function respondWithError($message, $statusCode = 400) 146 | { 147 | $error = [ 148 | 'message' => $message, 149 | 'status' => $statusCode 150 | ]; 151 | return response()->json($error, $statusCode); 152 | } 153 | 154 | private function respondWithAccount(Account $account, Cookie $accessTokenCookie, Cookie $refreshTokenCookie) 155 | { 156 | $properties = ['account'=>[]]; 157 | $config = config('stormpath.web.me.expand'); 158 | $whiteListResources = []; 159 | foreach($config as $item=>$value) { 160 | if($value == true) { 161 | $whiteListResources[] = $item; 162 | } 163 | } 164 | 165 | $propNames = $account->getPropertyNames(); 166 | foreach($propNames as $prop) { 167 | $property = $this->getPropertyValue($account, $prop); 168 | 169 | if(is_object($property) && !in_array($prop, $whiteListResources)) { 170 | continue; 171 | } 172 | 173 | $properties['account'][$prop] = $property; 174 | } 175 | return response()->json($properties)->cookie($accessTokenCookie)->cookie($refreshTokenCookie); 176 | } 177 | 178 | private function getPropertyValue($account, $prop) 179 | { 180 | $value = null; 181 | try { 182 | $value = $account->getProperty($prop); 183 | } catch (\Exception $e) {} 184 | 185 | return $value; 186 | 187 | } 188 | 189 | private function respondWithValidationErrorForJson($validator) 190 | { 191 | 192 | return response()->json([ 193 | 'message' => $validator->errors()->first(), 194 | 'status' => 400 195 | ], 400); 196 | } 197 | 198 | private function isSocialLoginAttempt() 199 | { 200 | $attempt = $this->request->has('providerData'); 201 | if(!$attempt) { 202 | return false; 203 | } 204 | switch ($provider = $this->request->input('providerData')['providerId']) 205 | { 206 | /** @codeCoverageIgnoreStart */ 207 | case 'google' : 208 | case 'facebook' : 209 | case 'linkedin' : 210 | return true; 211 | /** @codeCoverageIgnoreEnd */ 212 | case 'stormpath' : 213 | throw new \InvalidArgumentException("Please use the standard login/password method instead"); 214 | default : 215 | throw new \InvalidArgumentException("The social provider {$provider} is not supported"); 216 | } 217 | } 218 | /** @codeCoverageIgnore */ 219 | private function doSocialLogin() 220 | { 221 | switch ($provider = $this->request->input('providerData')['providerId']) 222 | { 223 | case 'google' : 224 | return app(SocialCallbackController::class)->google($this->request); 225 | case 'facebook' : 226 | return app(SocialCallbackController::class)->facebook($this->request); 227 | case 'linkedin' : 228 | return app(SocialCallbackController::class)->linkedin($this->request); 229 | } 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /src/Http/Controllers/RegisterController.php: -------------------------------------------------------------------------------- 1 | request = $request; 53 | $this->validator = app('validator');; 54 | } 55 | 56 | public function getRegister() 57 | { 58 | return $this->respondWithForm(); 59 | } 60 | 61 | public function postRegister() 62 | { 63 | $validator = $this->registerValidator(); 64 | 65 | if($validator->fails()) { 66 | return $this->respondWithValidationErrorForJson($validator); 67 | } 68 | 69 | if(($errorFields = $this->isAcceptedPostFields($this->request->all())) !== true) { 70 | return $this->respondWithErrorJson('We do not allow arbitrary data to be posted to an account\'s custom data object. `'. array_shift($errorFields) . '` is either disabled or not defined in the config.', 400); 71 | } 72 | 73 | 74 | try { 75 | $registerFields = $this->setRegisterFields(); 76 | // dd($registerFields); 77 | $account = \Stormpath\Resource\Account::instantiate($registerFields); 78 | 79 | app('cache.store')->forget('stormpath.application'); 80 | $application = app('stormpath.application'); 81 | 82 | $account = $application->createAccount($account); 83 | 84 | $customDataAdded = false; 85 | 86 | foreach ($registerFields as $key=>$value) { 87 | 88 | if ($key!='password' && $key!='confirmPassword') { 89 | if ($account->{$key}!=$registerFields[$key]) { 90 | $account->customData->{$key} = $value; 91 | $customDataAdded = true; 92 | } 93 | } 94 | } 95 | 96 | if ($customDataAdded) { 97 | $account->save(); 98 | } 99 | 100 | if(config('stormpath.web.register.autoLogin') == false) { 101 | return $this->respondWithAccount($account, new Cookie('access'), new Cookie('refresh')); 102 | } 103 | 104 | $login = isset($registerFields['username']) ? $registerFields['username'] : null; 105 | $login = isset($registerFields['email']) ? $registerFields['email'] : $login; 106 | 107 | $result = $this->authenticate($login, $registerFields['password']); 108 | $accessTokenCookie = $this->makeAccessTokenCookie($result->getAccessTokenString()); 109 | $refreshTokenCookie = $this->makeRefreshTokenCookie($result->getRefreshTokenString()); 110 | 111 | return $this->respondWithAccount($account, $accessTokenCookie, $refreshTokenCookie); 112 | 113 | 114 | } catch(\Stormpath\Resource\ResourceError $re) { 115 | return $this->respondWithErrorJson($re->getMessage(), $re->getStatus()); 116 | } 117 | } 118 | 119 | private function registerValidator() 120 | { 121 | $rules = []; 122 | $messages = []; 123 | 124 | $registerField = config('stormpath.web.register.form.fields'); 125 | 126 | foreach($registerField as $key => $field) { 127 | if($field['enabled'] == true && $field['required'] == true) { 128 | $rules[$key] = 'required'; 129 | } 130 | } 131 | 132 | $messages['username.required'] = 'Username is required.'; 133 | $messages['givenName.required'] = 'Given name is required.'; 134 | $messages['middleName.required'] = 'Middle name is required.'; 135 | $messages['surname.required'] = 'Surname is required.'; 136 | $messages['email.required'] = 'Email is required.'; 137 | $messages['password.required'] = 'Password is required.'; 138 | $messages['confirmPassword.required'] = 'Password confirmation is required.'; 139 | 140 | 141 | if( config('stormpath.web.register.form.fields.confirmPassword.enabled') ) { 142 | $rules['password'] = 'required|same:confirmPassword'; 143 | $messages['password.same'] = 'Passwords are not the same.'; 144 | } 145 | 146 | $validator = $this->validator->make( 147 | $this->request->all(), 148 | $rules, 149 | $messages 150 | ); 151 | 152 | 153 | return $validator; 154 | } 155 | 156 | private function setRegisterFields() 157 | { 158 | $registerArray = []; 159 | $registerFields = config('stormpath.web.register.form.fields'); 160 | foreach($registerFields as $spfield=>$field) { 161 | if($field['required'] == true) { 162 | $registerArray[$spfield] = $this->request->input($spfield); 163 | } 164 | } 165 | 166 | return $registerArray; 167 | } 168 | 169 | private function respondWithForm() 170 | { 171 | $fields = []; 172 | 173 | foreach(config('stormpath.web.register.form.fields') as $field) { 174 | if($field['enabled'] == true) { 175 | $fields[] = $field; 176 | } 177 | } 178 | 179 | $data = [ 180 | 'form' => [ 181 | 'fields' => $fields 182 | ], 183 | 'accountStores' => app('cache.store')->get('stormpath.accountStores') 184 | ]; 185 | 186 | return response()->json($data); 187 | } 188 | 189 | 190 | private function respondWithErrorJson($message, $statusCode = 400) 191 | { 192 | $error = [ 193 | 'message' => $message, 194 | 'status' => $statusCode 195 | ]; 196 | 197 | return response()->json($error, $statusCode); 198 | } 199 | 200 | 201 | private function respondWithAccount(Account $account, Cookie $accessTokenCookie, Cookie $refreshTokenCookie) 202 | { 203 | $properties = ['account'=>[]]; 204 | $config = config('stormpath.web.me.expand'); 205 | $whiteListResources = []; 206 | foreach($config as $item=>$value) { 207 | if($value == true) { 208 | $whiteListResources[] = $item; 209 | } 210 | } 211 | 212 | $propNames = $account->getPropertyNames(); 213 | foreach($propNames as $prop) { 214 | $property = $this->getPropertyValue($account, $prop); 215 | 216 | if(is_object($property) && !in_array($prop, $whiteListResources)) { 217 | continue; 218 | } 219 | 220 | $properties['account'][$prop] = $property; 221 | } 222 | 223 | return response()->json($properties)->cookie($accessTokenCookie)->cookie($refreshTokenCookie); 224 | } 225 | 226 | private function getPropertyValue($account, $prop) 227 | { 228 | $value = null; 229 | try { 230 | $value = $account->getProperty($prop); 231 | } catch (\Exception $e) {} 232 | 233 | return $value; 234 | 235 | } 236 | 237 | private function respondWithValidationErrorForJson($validator) 238 | { 239 | 240 | return response()->json([ 241 | 'message' => $validator->errors()->first(), 242 | 'status' => 400 243 | ], 400); 244 | } 245 | 246 | private function isAcceptedPostFields($submittedFields) 247 | { 248 | $fields = []; 249 | $allowedFields = config('stormpath.web.register.form.fields'); 250 | 251 | foreach($allowedFields as $key => $value) { 252 | //Enabled check when iOS SDK is updated to not use username in tests 253 | // if($value['enabled'] == false) continue; 254 | $fields[] = $key; 255 | } 256 | $fields[] = '_token'; 257 | 258 | if(!empty($diff = array_diff(array_keys($submittedFields), array_values($fields)))) { 259 | return $diff; 260 | } 261 | 262 | return true; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/Support/StormpathServiceProvider.php: -------------------------------------------------------------------------------- 1 | buildConfigFromDefaultYaml(); 44 | $this->buildClientConfig(); 45 | $this->buildApplicationConfig(); 46 | 47 | $this->registerClient(); 48 | $this->registerApplication(); 49 | $this->registerUser(); 50 | 51 | $this->disableCookieEncryption(); 52 | 53 | $this->registerCommands(); 54 | 55 | 56 | 57 | } 58 | 59 | public function boot() 60 | { 61 | $this->registerMiddleware(); 62 | $this->warmResources(); 63 | 64 | $this->checkForSocialProviders(); 65 | $this->setPasswordPolicies(); 66 | $this->setAccountCreationPolicy(); 67 | 68 | $this->loadRoutes(); 69 | } 70 | 71 | private function buildConfigFromDefaultYaml() 72 | { 73 | $config = dirname(__DIR__) . "/config/stormpath.yaml"; 74 | 75 | config(['stormpath' => Yaml::parse(file_get_contents($config))]); 76 | 77 | if(file_exists(base_path('/stormpath.yaml'))) { 78 | $config = Yaml::parse(file_get_contents(base_path('/stormpath.yaml'))); 79 | $newConfig = array_replace_recursive(config('stormpath'), $config); 80 | 81 | config(['stormpath'=>$newConfig]); 82 | } 83 | } 84 | 85 | public function buildClientConfig() 86 | { 87 | $clientConfig = [ 88 | 'client' => [ 89 | 'apiKey' => [ 90 | 'id' => env('STORMPATH_CLIENT_APIKEY_ID') ?: null, 91 | 'secret' => env('STORMPATH_CLIENT_APIKEY_SECRET') ?: null 92 | ] 93 | ] 94 | ]; 95 | 96 | $newConfig = array_merge(config('stormpath'), $clientConfig); 97 | config(['stormpath'=>$newConfig]); 98 | 99 | if(null === config('stormpath.client.apiKey.id') || null === config('stormpath.client.apiKey.secret')) { 100 | throw new \InvalidArgumentException("Please provide API Keys in your environment. STORMPATH_CLIENT_APIKEY_ID and STORMPATH_CLIENT_APIKEY_SECRET are required."); 101 | } 102 | } 103 | 104 | private function buildApplicationConfig() 105 | { 106 | $applicationConfig = [ 107 | 'application' => [ 108 | 'name' => env('STORMPATH_APPLICATION_NAME') ?: null, 109 | 'href' => env('STORMPATH_APPLICATION_HREF') ?: null 110 | ] 111 | ]; 112 | 113 | $newConfig = array_merge(config('stormpath'), $applicationConfig); 114 | config(['stormpath'=>$newConfig]); 115 | 116 | if(null === config('stormpath.application.href')) { 117 | throw new \InvalidArgumentException("Please provide an Application HREF in your environment. STORMPATH_APPLICATION_HREF is required."); 118 | } 119 | } 120 | 121 | private function registerClient() 122 | { 123 | $id = config( 'stormpath.client.apiKey.id' ); 124 | $secret = config( 'stormpath.client.apiKey.secret' ); 125 | 126 | Client::$apiKeyProperties = "apiKey.id={$id}\napiKey.secret={$secret}"; 127 | Client::$integration = $this->buildAgent(); 128 | 129 | 130 | $this->app->singleton('stormpath.client', function() { 131 | return Client::getInstance(); 132 | }); 133 | } 134 | 135 | private function registerApplication() 136 | { 137 | $this->app->singleton('stormpath.application', function() { 138 | $this->guardAgainstInvalidApplicationHref(); 139 | $application = \Stormpath\Resource\Application::get(config('stormpath.application.href')); 140 | return $application; 141 | }); 142 | 143 | } 144 | 145 | private function guardAgainstInvalidApplicationhref() 146 | { 147 | if (config('stormpath.application.href') == null) { 148 | throw new \InvalidArgumentException('Application href MUST be set.'); 149 | } 150 | 151 | if (!$this->isValidApplicationHref()) { 152 | throw new \InvalidArgumentException(config('stormpath.application.href') . ' is not a valid Stormpath Application HREF.'); 153 | } 154 | } 155 | 156 | private function isValidApplicationHref() 157 | { 158 | return !! strpos(config( 'stormpath.application.href' ), '/applications/'); 159 | } 160 | 161 | private function buildAgent() 162 | { 163 | $agent = []; 164 | 165 | if(app('request')->headers->has('X-STORMPATH-AGENT')) { 166 | $agent[] = app('request')->header('X-STORMPATH-AGENT'); 167 | } 168 | 169 | $version = $this->app->version(); 170 | 171 | $agent[] = self::INTEGRATION_NAME . '/' . self::INTEGRATION_VERSION; 172 | $agent[] = 'lumen/' . $version; 173 | 174 | return implode(' ', $agent); 175 | } 176 | 177 | private function registerUser() 178 | { 179 | $this->app->bind('stormpath.user', function($app) { 180 | 181 | try { 182 | $spApplication = app('stormpath.application'); 183 | } catch (\Exception $e) { 184 | return null; 185 | } 186 | 187 | $cookie = $app->request->cookie(config('stormpath.web.accessTokenCookie.name')); 188 | 189 | if(null === $cookie) { 190 | $cookie = $this->refreshCookie($app->request); 191 | } 192 | 193 | try { 194 | if($cookie instanceof \Symfony\Component\HttpFoundation\Cookie) { 195 | $cookie = $cookie->getValue(); 196 | } 197 | $result = (new \Stormpath\Oauth\VerifyAccessToken($spApplication))->verify($cookie); 198 | return $result->getAccount(); 199 | } catch (\Exception $e) {} 200 | 201 | return null; 202 | 203 | }); 204 | } 205 | 206 | private function disableCookieEncryption() 207 | { 208 | $this->app->resolving(EncryptCookies::class, function ($object) { 209 | $object->disableFor(config('stormpath.web.accessTokenCookie.name')); 210 | $object->disableFor(config('stormpath.web.refreshTokenCookie.name')); 211 | }); 212 | } 213 | 214 | private function warmResources() 215 | { 216 | if(config('stormpath.application.href') == null) return; 217 | $cache = $this->app['cache.store']; 218 | 219 | if($cache->has('stormpath.resourcesWarm') && $cache->get('stormpath.resourcesWarm') == true) return; 220 | 221 | app('stormpath.client'); 222 | $application = app('stormpath.application'); 223 | 224 | $dasm = AccountStoreMapping::get($application->defaultAccountStoreMapping->href); 225 | 226 | $mappings = $application->getAccountStoreMappings(['expand'=>'accountStore']); 227 | $accountStoreArray = []; 228 | 229 | foreach($mappings as $mapping) { 230 | $accountStoreArrayValues = [ 231 | 'href' => $mapping->accountStore->href, 232 | 'name' => $mapping->accountStore->name 233 | ]; 234 | if(isset($mapping->accountStore->provider)) { 235 | $accountStoreArrayValues['provider'] = [ 236 | 'href' => $mapping->accountStore->provider->href, 237 | 'providerId' => $mapping->accountStore->provider->providerId 238 | ]; 239 | } 240 | $accountStoreArray[] = $accountStoreArrayValues; 241 | } 242 | 243 | 244 | $asm = AccountStoreMapping::get($application->accountStoreMappings->href,['expand'=>'accountStore']); 245 | 246 | $passwordPolicy = $dasm->getAccountStore()->getProperty('passwordPolicy'); 247 | 248 | $accountCreationPolicy = $dasm->getAccountStore(['expand'=>'accountCreationPolicy'])->accountCreationPolicy; 249 | 250 | $passwordPolicies = PasswordPolicies::get($passwordPolicy->href); 251 | 252 | 253 | $cache->rememberForever('stormpath.defaultAccountStoreMapping', function() use ($dasm) { 254 | return $dasm; 255 | }); 256 | 257 | $cache->rememberForever('stormpath.accountStoreMappings', function() use ($asm) { 258 | return $asm; 259 | }); 260 | 261 | $cache->rememberForever('stormpath.accountStores', function() use ($accountStoreArray) { 262 | return $accountStoreArray; 263 | }); 264 | 265 | $cache->rememberForever('stormpath.passwordPolicy', function() use ($passwordPolicy) { 266 | return $passwordPolicy; 267 | }); 268 | 269 | $cache->rememberForever('stormpath.accountCreationPolicy', function() use ($accountCreationPolicy) { 270 | return $accountCreationPolicy; 271 | }); 272 | 273 | $cache->rememberForever('stormpath.passwordPolicies', function() use ($passwordPolicies) { 274 | return $passwordPolicies; 275 | }); 276 | 277 | $cache->rememberForever('stormpath.resourcesWarm', function() { 278 | return true; 279 | }); 280 | } 281 | 282 | private function checkForSocialProviders() 283 | { 284 | if(config('stormpath.application.href') == null) return; 285 | 286 | $model = app('cache.store')->rememberForever('stormpath.idsitemodel', function() { 287 | $idSiteModel = $this->getIdSiteModel(); 288 | return IdSiteModel::get($idSiteModel->href); 289 | }); 290 | 291 | $providers = $model->getProperty('providers'); 292 | 293 | 294 | foreach($providers as $provider) { 295 | config(['stormpath.web.social.enabled' => true]); 296 | 297 | switch ($provider->providerId) { 298 | case 'facebook' : 299 | $this->setupFacebookProvider($provider); 300 | break; 301 | case 'google' : 302 | $this->setupGoogleProvider($provider); 303 | break; 304 | case 'linkedin' : 305 | $this->setupLinkedinProvider($provider); 306 | break; 307 | } 308 | } 309 | } 310 | 311 | private function getIdSiteModel() 312 | { 313 | $model = app('stormpath.application')->getProperty('idSiteModel'); 314 | 315 | if($model == null) { 316 | throw new \InvalidArgumentException('ID Site could not initialize, please visit ID Site from the Stormpath Dashboard and then clear your cache'); 317 | } 318 | 319 | return $model; 320 | 321 | } 322 | 323 | private function setupFacebookProvider($provider) 324 | { 325 | config(['stormpath.web.social.facebook.enabled' => true]); 326 | config(['stormpath.web.social.facebook.name' => 'Facebook']); 327 | config(['stormpath.web.social.facebook.clientId' => $provider->clientId]); 328 | } 329 | 330 | private function setupGoogleProvider($provider) 331 | { 332 | config(['stormpath.web.social.google.enabled' => true]); 333 | config(['stormpath.web.social.google.name' => 'Google']); 334 | config(['stormpath.web.social.google.clientId' => $provider->clientId]); 335 | config(['stormpath.web.social.google.callbackUri' => $provider->redirectUri]); 336 | } 337 | 338 | private function setupLinkedinProvider($provider) 339 | { 340 | config(['stormpath.web.social.linkedin.enabled' => true]); 341 | config(['stormpath.web.social.linkedin.name' => 'LinkedIn']); 342 | config(['stormpath.web.social.linkedin.clientId' => $provider->clientId]); 343 | 344 | } 345 | 346 | private function setPasswordPolicies() 347 | { 348 | if(config('stormpath.web.forgotPassword.enabled') == true) return; 349 | 350 | if(config('stormpath.web.changePassword.enabled') == true) return; 351 | 352 | if(config('stormpath.application.href') == null) return; 353 | 354 | config(['stormpath.web.forgotPassword.enabled' => false]); 355 | config(['stormpath.web.changePassword.enabled' => false]); 356 | 357 | $cache = $this->app['cache.store']; 358 | 359 | $passwordPolicies = $cache->get('stormpath.passwordPolicies'); 360 | 361 | if($passwordPolicies->getProperty('resetEmailStatus') == Stormpath::ENABLED) { 362 | config(['stormpath.web.forgotPassword.enabled' => true]); 363 | config(['stormpath.web.changePassword.enabled' => true]); 364 | return; 365 | } 366 | } 367 | 368 | private function setAccountCreationPolicy() 369 | { 370 | if(config('stormpath.web.verifyEmail.enabled') == true) return; 371 | 372 | $cache = $this->app['cache.store']; 373 | 374 | if(!$cache->has('stormpath.accountCreationPolicy')) { 375 | $this->warmResources(); 376 | } 377 | 378 | config(['stormpath.web.verifyEmail.enabled' => false]); 379 | 380 | $accountCreationPolicy = $cache->get('stormpath.accountCreationPolicy'); 381 | 382 | if($accountCreationPolicy == null) { 383 | return; 384 | } 385 | 386 | if($accountCreationPolicy->verificationEmailStatus == Stormpath::ENABLED) { 387 | config(['stormpath.web.verifyEmail.enabled' => true]); 388 | } 389 | 390 | if(config('stormpath.web.verifyEmail.enabled') && config('stormpath.web.register.autoLogin')) { 391 | throw new \InvalidArgumentException('AutoLogin and Verify are both enabled. Either disable Auto Login, or turn off verification email.'); 392 | } 393 | } 394 | 395 | private function loadRoutes() 396 | { 397 | require __DIR__ . '/../Http/routes.php'; 398 | 399 | if(config('stormpath.web.social.enabled')) { 400 | require __DIR__ . '/../Http/socialRoutes.php'; 401 | } 402 | } 403 | 404 | 405 | private function registerCommands() 406 | { 407 | $this->app->singleton('stormpath.config.command', function() 408 | { 409 | return new StormpathConfigCommand(); 410 | }); 411 | 412 | $this->commands( 413 | 'stormpath.config.command' 414 | ); 415 | } 416 | 417 | private function registerMiddleware() 418 | { 419 | $this->app->routeMiddleware(['stormpath.auth' => \Stormpath\Lumen\Http\Middleware\Authenticate::class]); 420 | $this->app->routeMiddleware(['stormpath.guest' => \Stormpath\Lumen\Http\Middleware\RedirectIfAuthenticated::class]); 421 | 422 | } 423 | 424 | 425 | } --------------------------------------------------------------------------------