getAttribute('provider'); $provider = $this->getProvider($providerName); $config = $this->config->get('oauth')[$providerName]; // Handle OAuth error response according to https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1 if ($request->has('error')) { throw new HttpNotFound('oauth.' . $request->get('error')); } if (!$request->has('code')) { $authorizationUrl = $provider->getAuthorizationUrl( [ // League oauth separates scopes by comma, which is wrong, so we do it // here properly by spaces. See https://www.rfc-editor.org/rfc/rfc6749#section-3.3 'scope' => join(' ', $config['scope'] ?? []), ] ); $this->session->set('oauth2_state', $provider->getState()); return $this->redirect->to($authorizationUrl); } if ( !$this->session->get('oauth2_state') || $request->get('state') !== $this->session->get('oauth2_state') ) { $this->session->remove('oauth2_state'); $this->log->warning('Invalid OAuth state'); throw new HttpNotFound('oauth.invalid-state'); } $accessToken = null; try { $accessToken = $provider->getAccessToken( 'authorization_code', [ 'code' => $request->get('code'), ] ); } catch (IdentityProviderException $e) { $this->handleOAuthError($e, $providerName); } $resourceOwner = null; try { $resourceOwner = $provider->getResourceOwner($accessToken); } catch (IdentityProviderException $e) { $this->handleOAuthError($e, $providerName); } $resourceId = $this->getId($providerName, $resourceOwner); /** @var OAuth|null $oauth */ $oauth = $this->oauth ->query() ->where('provider', $providerName) ->where('identifier', $resourceId) ->get() // Explicit case-sensitive comparison using PHP as some DBMS collations are case-sensitive and some aren't ->where('identifier', '===', (string) $resourceId) ->first(); $expirationTime = $accessToken->getExpires(); $expirationTime = $expirationTime ? Carbon::createFromTimestamp($expirationTime) : null; if ($oauth) { $oauth->access_token = $accessToken->getToken(); $oauth->refresh_token = $accessToken->getRefreshToken(); $oauth->expires_at = $expirationTime; $oauth->save(); } $user = $this->auth->user(); if ($oauth && $user && $user->id != $oauth->user_id) { throw new HttpNotFound('oauth.already-connected'); } $connectProvider = $this->session->get('oauth2_connect_provider'); $this->session->remove('oauth2_connect_provider'); if (!$oauth && $user && $connectProvider && $connectProvider == $providerName) { $oauth = new OAuth([ 'provider' => $providerName, 'identifier' => $resourceId, 'access_token' => $accessToken->getToken(), 'refresh_token' => $accessToken->getRefreshToken(), 'expires_at' => $expirationTime, ]); $oauth->user() ->associate($user) ->save(); $this->log->info( 'Connected OAuth user {user} using {provider}', ['provider' => $providerName, 'user' => $resourceId] ); $this->addNotification('oauth.connected'); } $resourceData = $resourceOwner->toArray(); if (!empty($config['nested_info'])) { $resourceData = Arr::dot($resourceData); } $userdata = new Collection($resourceData); if (!$oauth) { return $this->redirectRegister( $providerName, (string) $resourceId, $accessToken, $config, $userdata ); } if (isset($config['mark_arrived']) && $config['mark_arrived']) { $this->handleArrive($providerName, $oauth, $resourceOwner); } $response = $this->authController->loginUser($oauth->user); event('oauth2.login', ['provider' => $providerName, 'data' => $userdata]); return $response; } public function connect(Request $request): Response { $providerName = $request->getAttribute('provider'); $this->requireProvider($providerName); $this->session->set('oauth2_connect_provider', $providerName); return $this->index($request); } public function disconnect(Request $request): Response { $providerName = $request->getAttribute('provider'); $this->oauth ->whereUserId($this->auth->user()->id) ->where('provider', $providerName) ->delete(); $this->log->info('Disconnected OAuth from {provider}', ['provider' => $providerName]); $this->addNotification('oauth.disconnected'); return $this->redirect->back(); } protected function getProvider(string $name): AbstractProvider { $this->requireProvider($name); $config = $this->config->get('oauth')[$name]; return new GenericProvider( [ 'clientId' => $config['client_id'], 'clientSecret' => $config['client_secret'], 'redirectUri' => $this->url->to('oauth/' . $name), 'urlAuthorize' => $config['url_auth'], 'urlAccessToken' => $config['url_token'], 'urlResourceOwnerDetails' => $config['url_info'], 'responseResourceOwnerId' => $config['id'], ] ); } protected function getId(string $providerName, ResourceOwner $resourceOwner): mixed { $config = $this->config->get('oauth')[$providerName]; if (empty($config['nested_info'])) { return $resourceOwner->getId(); } $data = Arr::dot($resourceOwner->toArray()); return $data[$config['id']]; } protected function requireProvider(string $provider): void { if (!$this->isValidProvider($provider)) { throw new HttpNotFound('oauth.provider-not-found'); } } protected function isValidProvider(string $name): bool { $config = $this->config->get('oauth'); return isset($config[$name]); } protected function handleArrive( string $providerName, OAuth $auth, ResourceOwner $resourceOwner ): void { $user = $auth->user; $userState = $user->state; if ($userState->arrived) { return; } $userState->arrived = true; $userState->arrival_date = new Carbon(); $userState->save(); $this->log->info( 'Set user {name} ({id}) as arrived via {provider} user {user}', [ 'provider' => $providerName, 'user' => $this->getId($providerName, $resourceOwner), 'name' => $user->name, 'id' => $user->id, ] ); } /** * * @throws HttpNotFound */ protected function handleOAuthError(IdentityProviderException $e, string $providerName): void { $response = $e->getResponseBody(); $response = is_array($response) ? json_encode($response) : $response; $this->log->error( '{provider} identity provider error: {error} {description}', [ 'provider' => $providerName, 'error' => $e->getMessage(), 'description' => $response, ] ); throw new HttpNotFound('oauth.provider-error'); } protected function redirectRegister( string $providerName, string $providerUserIdentifier, AccessTokenInterface $accessToken, array $config, Collection $userdata ): Response { $config = array_merge( [ 'username' => null, 'email' => null, 'first_name' => null, 'last_name' => null, 'enable_password' => false, 'allow_registration' => null, 'groups' => null, ], $config ); if (!$this->config->get('registration_enabled') && !$config['allow_registration']) { throw new HttpNotFound('oauth.not-found'); } $this->session->set( 'form_data', [ 'name' => $userdata->get($config['username']), 'email' => $userdata->get($config['email']), 'first_name' => $userdata->get($config['first_name']), 'last_name' => $userdata->get($config['last_name']), ], ); $this->session->set('oauth2_groups', $userdata->get($config['groups'], [])); $this->session->set('oauth2_connect_provider', $providerName); $this->session->set('oauth2_user_id', $providerUserIdentifier); $expirationTime = $accessToken->getExpires(); $expirationTime = $expirationTime ? Carbon::createFromTimestamp($expirationTime) : null; $this->session->set('oauth2_access_token', $accessToken->getToken()); $this->session->set('oauth2_refresh_token', $accessToken->getRefreshToken()); $this->session->set('oauth2_expires_at', $expirationTime); $this->session->set('oauth2_enable_password', $config['enable_password']); $this->session->set('oauth2_allow_registration', $config['allow_registration']); return $this->redirect->to('/sign-up'); } }