app = $app; } // API /** * @param $method * @param $uri * @param null $data * @return array * @throws ConnectException */ public function request($method, $uri, $data = null, $access_token = null): array { $url = $this->getUrl(); $url = $url . '/' . trim($uri, '/'); $client = new Client(); $config = [ 'headers' => [ 'Accept' => 'application/json' ] ]; if (! is_null($access_token)) { $config['headers']['Authorization'] = 'Bearer ' . $access_token; } if (! is_null($data)) { $config['form_params'] = $data; } try { $response = $client->request($method, $url, $config); $body = (string) $response->getBody(); if ($response->getStatusCode() !== 200) throw new ConnectException($body); return json_decode($body, true); } catch(\Exception $e) { $this->flushTokens(); throw new ConnectException($e->getMessage()); } } // Authorization flow public function redirect($state = null) { if (is_null($state)) $state = Str::random(); $states = session()->get('connect_states'); if (! is_array($states)) $states = []; $states[] = $state; session()->put('connect_states', $states); $query = http_build_query([ 'client_id' => config('bconnect.client_id'), 'scope' => config('bconnect.scopes'), 'redirect_uri' => config('bconnect.redirect_url'), 'response_type' => 'code', 'state' => $state ]); $url = $this->getUrl() . '/oauth/authorize?' . $query; return redirect($url); } public function checkState(Request $request) { if (! session()->has('connect_states')) return false; $states = session()->get('connect_states'); if (! is_array($states)) return false; if (! $request->has('state') || ! in_array($request->state, $states)) return false; unset($states[array_search($request->state, $states)]); session()->put('connect_states', $states); return true; } public function loginFromCallback(Request $request, $redirect_to = '/') { if (! $this->checkState($request) || ! $request->has('code')) return redirect('/'); // Access token $expires_at = now(); $connect_data = $this->getAccessTokenFromAuthorizationCode($request->get('code')); $connect_data['expires_at'] = $expires_at->addSeconds($connect_data['expires_in']); // User data $user_data = $this->getUserData($connect_data['access_token']); $user = $this->sync('create', $user_data); $this->updateUserConnectData($user, $connect_data); $user->save(); // Login auth()->login($user, config('bconnect.login_remember', true)); return redirect($redirect_to); } // OAuth methods public function getAccessTokenFromAuthorizationCode($code) { return $this->request('post', 'oauth/token', [ 'grant_type' => 'authorization_code', 'client_id' => config('bconnect.client_id'), 'client_secret' => config('bconnect.client_secret'), 'scope' => config('bconnect.user_scopes'), 'redirect_uri' => config('bconnect.redirect_url'), 'code' => $code ]); } public function getAccessTokenFromRefreshToken($refresh_token) { return $this->request('post', 'oauth/token', [ 'grant_type' => 'refresh_token', 'client_id' => config('bconnect.client_id'), 'client_secret' => config('bconnect.client_secret'), 'scope' => config('bconnect.user_scopes'), 'redirect_uri' => config('bconnect.redirect_url'), 'refresh_token' => $refresh_token ]); } public function getUserData($access_token) { return $this->request('get', 'api/user', null, $access_token); } public function getUserAccessToken($model) { $class = get_class($model); $has_fields = in_array(HasConnectTokens::class, class_uses($class)); if (! $has_fields) { throw new ConnectException("$class does not implement HasConnectTokens"); } if ($model->connect_expires_at <= now()->addHour()) { $connect_data = $this->getAccessTokenFromRefreshToken($model->connect_refresh_token); $this->updateUserConnectData($model, $connect_data); return $connect_data['access_token']; } return $model->connect_access_token; } // Sync public function sync(string $event, array $data) { $class = config('bconnect.model'); if (in_array($event, ['update', 'delete'])) { $model = $this->resolveUser($data); if (! $model->exists() && $event === 'delete') return $model; } else { $hasSoftDeletes = in_array(\Illuminate\Database\Eloquent\SoftDeletes::class, class_uses($class)); $model = $this->resolveUser($data, $hasSoftDeletes); if ($model->exists()) { if ($hasSoftDeletes && $model->trashed()) { $event = 'restore'; } else { $event = 'update'; } } } if (in_array(HasConnectWebhook::class, class_uses($class))) { $method = 'onConnect' . ucfirst($event); $model->$method($data); } else { $this->updateUserData($model, $data); $model->save(); if (in_array(HasConnectData::class, class_uses($model))) { $model->postFillConnectData($data); } } return $model; } public function updateUserConnectData($user, $data) { if (in_array(HasConnectTokens::class, class_uses(get_class($user)))) $user->fillConnectTokens($data); } public function updateUserData($user, $data) { if (in_array(HasConnectData::class, class_uses(get_class($user)))) $user->fillConnectData($data); } protected function resolveUser($data, $withTrashed = false) { $class = config('bconnect.model'); $query = $class::query(); if ($withTrashed) $query->withTrashed(); $model = new $class; if (in_array(HasConnectData::class, class_uses($class))) { $id = $model->getConnectIdentifier(); $origin = is_array($id) ? $id[0] : $id; $target = is_array($id) ? $id[1] : $id; $model = $query->where($target, $data[$origin])->first() ?? $model; $model->$target = $data[$origin]; } else { $model = $query->where('email', $data['email'])->first() ?? $model; if (! $model->exists()) $model->email = $data['email']; } return $model; } // Routing public function routes() { Route::namespace('\Bluesquare\Connect\Controllers') ->group(function () { Route::get('connect/authorize', 'ConnectController@authorize'); Route::get('connect/callback', 'ConnectController@callback'); }); } public function apiRoutes() { Route::namespace('\Bluesquare\Connect\Controllers') ->group(function () { Route::post('connect/webhook', 'ConnectController@webhook'); }); } // Misc protected function flushTokens() { session()->forget('bconnect.access_token'); session()->forget('bconnect.access_token_expiration'); } protected function getUrl() { return config('bconnect.url') ?? 'https://connect.bluesquare.io'; } }