DEV Community

Cover image for How To Integrate Google Calendar API and friendship with Laravel. Part 2
Denis Sinyukov
Denis Sinyukov

Posted on • Updated on • Originally published at coderden.dev

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


Enter fullscreen mode Exit fullscreen mode

The next step is to add the following variables to your .env file and add configuration to services.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


Enter fullscreen mode Exit fullscreen mode


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,
    ],
];



Enter fullscreen mode Exit fullscreen mode

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:



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


Enter fullscreen mode Exit fullscreen mode

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 {}


Enter fullscreen mode Exit fullscreen mode

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);
}



Enter fullscreen mode Exit fullscreen mode

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;
    }
}



Enter fullscreen mode Exit fullscreen mode

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']
        );
    }
}


Enter fullscreen mode Exit fullscreen mode

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']);


Enter fullscreen mode Exit fullscreen mode

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



Enter fullscreen mode Exit fullscreen mode

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', '')
);


Enter fullscreen mode Exit fullscreen mode

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));


Enter fullscreen mode Exit fullscreen mode

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);


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode


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);
        }
    }


Enter fullscreen mode Exit fullscreen mode

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

Top comments (3)

Collapse
 
yanyiik99 profile image
yanyiik99

hi do you have any reference for codeigniter 4?

Collapse
 
dnsinyukov profile image
Denis Sinyukov

hey, unfortunately, no

Collapse
 
yanyiik99 profile image
yanyiik99

p