How To Integrate Google Calendar API and friendship with Laravel. Part 2
Tags: laravel, php, webdev, googlecloud
Published: 2022-12-10 23:39:32

In the [previous article](https://dev.to/dnsinyukov/how-to-integrate-google-calendar-api-and-friendship-with-laravel-4cok) 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.

```php
composer require google/apiclient
```
The next step is to add the following variables to your `.env` file and add configuration to `services.php`:

```php
.env

GOOGLE_CLIENT_ID= #from .json file
GOOGLE_CLIENT_SECRET= #from .json file
GOOGLE_REDIRECT_URI= #from .json file
GOOGLE_REDIRECT_CALLBACK=https://localhost/oauth2 #redirect URL after fetching userinfo
GOOGLE_APPROVAL_PROMPT=force
GOOGLE_ACCESS_TYPE=offline
```

```php
services.php

return [
    '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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p1al4q2o28y3rmxedgca.jpg)

You can use [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes#calendar):
![Google Calendar OAuth 2 scopes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uv1rshynhqj1ty9vsbtc.jpg)
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:

```php
openid
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/calendar.events.readonly
https://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.

```php
interface ProviderInterface {}
```

Polymorphism will help us create an adaptive application. 

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

```php

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.

```php

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.

```php
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. 


```php
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/](https://localhost/oauth2/google/).

```php

    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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cxzjaejaog4hp57khr76.jpg)
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.

```php
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`.

```php
$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.

```php
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.

```php
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();
});
```

```php
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mvrkl81kwiiaeyqic1az.jpg)

## 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](https://github.com/dnsinyukov/sync-calendars/commit/9dc44e753ecf9dedcfe7ed15dd148dab1e50e062).

## Related links
- [How To Integrate Google Calendar API and friendship with Laravel. Part 1](https://dev.to/dnsinyukov/how-to-integrate-google-calendar-api-and-friendship-with-laravel-4cok)