Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
December 10, 2022 11:39 pm GMT

How To Integrate Google Calendar API and friendship with Laravel. Part 2

In the previous article we created a project in the Google Cloud Console, as well as configured the access keys through the API.

In this article we'll create a project in Laravel that will authorize through Google and save access token to database. You will learn the basic queries to Google via the OAuth 2.0 protocol.

Let's skip creating the basic skeleton of a Laravel framework and start immediately with creating code to work with the Google API.

Creating the application

First, we need to install the necessary libraries to work with the API. The library allows us to work with Google API Services without unnecessary actions. You can also use a simple Guzzle or CURL client.

composer require google/apiclient

The next step is to add the following variables to your .env file and add configuration to services.php:

.envGOOGLE_CLIENT_ID= #from .json fileGOOGLE_CLIENT_SECRET= #from .json fileGOOGLE_REDIRECT_URI= #from .json fileGOOGLE_REDIRECT_CALLBACK=https://localhost/oauth2 #redirect URL after fetching userinfoGOOGLE_APPROVAL_PROMPT=forceGOOGLE_ACCESS_TYPE=offline
services.phpreturn [    'google' => [        'client_id' => env('GOOGLE_CLIENT_ID'),        'client_secret' => env('GOOGLE_CLIENT_SECRET'),        'redirect_uri' => env('GOOGLE_REDIRECT_URI'),        'redirect_callback' => env('GOOGLE_REDIRECT_CALLBACK'),        'scopes' => [            \Google_Service_Calendar::CALENDAR_EVENTS_READONLY,            \Google_Service_Calendar::CALENDAR_READONLY,            \Google_Service_Oauth2::OPENID,            \Google_Service_Oauth2::USERINFO_EMAIL,            \Google_Service_Oauth2::USERINFO_PROFILE,        ],        'approval_prompt' => env('GOOGLE_APPROVAL_PROMPT', 'force'),        'access_type' => env('GOOGLE_ACCESS_TYPE', 'offline'),        'include_granted_scopes' => true,    ],];

OAuth process

After creating and setting up our service, we need to log in through the OAuth process. To do that, we need to generate auth URL and redirect the user to the Google OAuth 2.0 server to begin the authentication and authorization process.

OAuth process

You can use OAuth 2.0 Scopes for Google APIs:
Google Calendar OAuth 2 scopes
The first column indicates the name of the scope, a list of which we have defined in the settings. Google will require consent for each when you authorize.

In our services.php, we requested permission to:

openidhttps://www.googleapis.com/auth/userinfo.emailhttps://www.googleapis.com/auth/userinfo.profilehttps://www.googleapis.com/auth/calendar.events.readonlyhttps://www.googleapis.com/auth/calendar.readonly

Google as a Driver

We'll create a service to work with the Google API that will complement and encapsulate the way the app works and add some polymorphism to it.

A little later, we will expand the work of calendars to other providers, such as Outlook.

Create a Marker Interface (Tag Interface). In the future we will supplement it with the necessary methods.

Marker Interfaces are empty interfaces, i.e, they do not have any variables or methods declared in them.

interface ProviderInterface {}

Polymorphism will help us create an adaptive application.

Let's create a basic service to work with all popular calendars.

abstract class AbstractProvider implements ProviderInterface{    protected $providerName;    protected $request;    protected $httpClient;    protected $clientId;    protected $clientSecret;    protected $redirectUrl;    protected $scopes = [];    protected $scopeSeparator = ' ';    protected $user;    /**     * Create a new provider instance.     */    public function __construct(Request $request, string $clientId, string $clientSecret, string $redirectUrl, array $scopes = [])    {        $this->request = $request;        $this->clientId = $clientId;        $this->redirectUrl = $redirectUrl;        $this->clientSecret = $clientSecret;        $this->scopes = $scopes;    }    /**     * @return RedirectResponse     * @throws \Exception     */    public function redirect(): RedirectResponse    {        $this->request->query->add(['state' => $this->getState()]);        if ($user = $this->request->user()) {            $this->request->query->add(['user_id' => $user->getKey()]);        }        return new RedirectResponse($this->createAuthUrl());    }    /**     * @return User     */    public function getUser(): User    {        if (isset($this->user)) {            return $this->user;        }        try {            $credentials = $this->fetchAccessTokenWithAuthCode(                $this->request->get('code', '')            );            $this->user = $this->toUser($this->getBasicProfile($credentials));        } catch (\Exception $exception) {            report($exception);            throw new \InvalidArgumentException($exception->getMessage());        }        $state = $this->request->get('state', '');        if (isset($state)) {            $state = Crypt::decrypt($state);        }        return $this->user            ->setRedirectCallback($state['redirect_callback'])            ->setToken($credentials['access_token'])            ->setRefreshToken($credentials['refresh_token'])            ->setExpiresAt(                Carbon::now()->addSeconds($credentials['expires_in'])            )            ->setScopes(                explode($this->getScopeSeparator(), $credentials['scope'])            );    }    abstract protected function createAuthUrl();    abstract protected function fetchAccessTokenWithAuthCode(string $code);    abstract protected function getBasicProfile($credentials);    abstract protected function toUser($userProfile);}

Our first implementation will be a service for the work of Google, let's create it.

class GoogleProvider extends AbstractProvider{    protected $providerName = 'google';    public function createAuthUrl(): string    {        return $this->getHttpClient()->createAuthUrl();    }    public function redirect(): RedirectResponse    {        if ($redirectCallback = config('services.google.redirect_callback')) {            $this->request->query->add(['redirect_callback' => $redirectCallback]);        }        return parent::redirect();    }    protected function fetchAccessTokenWithAuthCode(string $code): array    {        return $this->getHttpClient()->fetchAccessTokenWithAuthCode($code);    }    /**     * @return array     */    protected function getBasicProfile($credentials)    {        $jwt = explode('.', $credentials['id_token']);        // Extract the middle part, base64 decode it, then json_decode it        return json_decode(base64_decode($jwt[1]), true);    }    /**     * @param Userinfo $userProfile     * @return void     */    protected function toUser($userProfile)    {        return tap(new User(), function ($user) use ($userProfile) {            $user->setId($userProfile['sub']);            $user->setName($userProfile['name']);            $user->setEmail($userProfile['email']);            $user->setPicture($userProfile['picture']);        });    }    /**     * @return Client     */    protected function getHttpClient(): Client    {        if (is_null($this->httpClient)) {            $this->httpClient = new \Google\Client();            $this->httpClient->setApplicationName(config('app.name'));            $this->httpClient->setClientId($this->clientId);            $this->httpClient->setClientSecret($this->clientSecret);            $this->httpClient->setRedirectUri($this->redirectUrl);            $this->httpClient->setScopes($this->scopes);            $this->httpClient->setApprovalPrompt(config('services.google.approval_prompt'));            $this->httpClient->setAccessType(config('services.google.access_type'));            $this->httpClient->setIncludeGrantedScopes(config('services.google.include_granted_scopes'));            // Add request query to the state            $this->httpClient->setState(                Crypt::encrypt($this->request->all())            );        }        return $this->httpClient;    }}

The only thing missing is a manager to work with our service. Now we can safely inherit from our base class AbstractProvider and implement new drivers.

use Illuminate\Support\Manager;class CalendarManager extends Manager {    protected function createGoogleDriver(): ProviderInterface    {        $config = $this->config->get('services.google');        return $this->buildProvider(GoogleProvider::class, $config);    }    protected function buildProvider($provider, $config): ProviderInterface    {        return new $provider(            $this->container->make('request'),            $config['client_id'],            $config['client_secret'],            $config['redirect_uri'],            $config['scopes']        );    }}

The business logic for getting the data about the Google user and access tokens is almost ready. All that remains is to test it in a live environment.

All that's left to do is make our services accessible by creating new routes and controllers.

Route::name('oauth2.auth')->get('/oauth2/{provider}', [AccountController::class, 'auth']);Route::name('oauth2.callback')->get('/oauth2/{provider}/callback', [AccountController::class, 'callback']);

The provider parameter will be dynamically inserted and checked against the CalendarManager class.

Let's start by redirecting the user to the Google consent screen using AccountController@auth() function. Click on the link https://localhost/oauth2/google/.

    public function auth(string $driver): RedirectResponse    {        try {            return app(CalendarManager::class)->driver($driver)->redirect();        } catch (\InvalidArgumentException $exception) {            report($exception);            abort(400, $exception->getMessage());        }    }

The CalendarManager will find the right provider from the URL and create the necessary driver to work with Google and will send a request for an authorization grant. (Step 1,2)
OAuth2 flow
As soon as the user logs in and agrees with the scopes of our app (reading profile, email, calendars, events), they are redirected back to our AccountController@callback, which we specified in the .env file, we get a code in the body of our response from the Google Auth server.

The getUser() method, based on the code will request an access token and a refresh token with which we can retrieve the data.

public function getUser():$credentials = $this->fetchAccessTokenWithAuthCode(    $this->request->get('code', ''));

Next, the access token allows you to request private info (step 5,6). At the first request we need to get information about the owner of the account, his name, email and ID.

$this->user = $this->toUser($this->getBasicProfile($credentials));

We encode the access token and refresh tokens via jwt, and store them in the database along with the profile data. The JWT token will store all token validity information.

public function encode(array $payload): string    {        $config = config('app');        $tokenId = base64_encode(random_bytes(16));        $issuedAt = new \DateTimeImmutable();        $jwtPayload = [            'iat'  => $issuedAt->getTimestamp(),            'jti'  => $tokenId,            'iss'  => $config['name'],            'nbf'  => $issuedAt->getTimestamp(),            'exp'  => $payload['expires_at']->getTimestamp(),            'data' => [                'access_token' => $payload['access_token'],                'refresh_token' => $payload['refresh_token'],                'provider' => $payload['provider'],                'scopes' => $payload['scopes'],                'email' => $payload['email'],                'account_id' => $payload['account_id'],            ]        ];        return JWT::encode($jwtPayload, $config['key'], $this->alg);

And save everything in the database for later use.

Schema::create('oauth2_accounts', function (Blueprint $table) {    $table->id();    $table->string('account_id', 100);    $table->string('name');    $table->string('email')->index();    $table->string('picture');    $table->string('provider')->nullable();    $table->unsignedBigInteger('user_id')->nullable();    $table->text('token')->nullable();    $table->dateTime('expires_at')->nullable();    $table->timestamps();});
UserService.php    public function saveFromUser(User $user, string $provider)    {        $payload = [            'account_id' => $user->getId(),            'email' => $user->getEmail(),            'name' => $user->getName(),            'picture' => $user->getPicture(),            'provider' => $provider,            'access_token' => $user->getAccessToken(),            'refresh_token' => $user->getRefreshToken(),            'scopes' => implode(' ', $user->getScopes()),            'expires_at' => $user->getExpiresAt(),            'created_at' => now(),            'updated_at' => now()        ];        $payload['token'] = $this->encrypter->encode($payload);        unset($payload['access_token'], $payload['refresh_token'], $payload['scopes']);        if (DB::table('oauth2_accounts')            ->where('account_id', $payload['account_id'])            ->where('provider', $provider)            ->exists()        ) {            unset($payload['created_at']);            DB::table('oauth2_accounts')                ->where('account_id', $payload['account_id'])                ->where('provider', $provider)                ->update($payload);        } else {            DB::table('oauth2_accounts')->insert($payload);        }    }

OAuth Accounts table

Bottom Line

In this article we created the application and the driver to work with the Google API, as well as authorized the user through OAuth 2 from Google and got tokens to access the server. We will need tokens to get data at any time. This approach will allow us to use access to API in any application, be it WEB or mobile.

You can find the full source code in the Laravel package at the GitHub.

Related links


Original Link: https://dev.to/dnsinyukov/how-to-integrate-google-calendar-api-and-friendship-with-laravel-part-2-48ba

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To