DEV Community

Cover image for accessing google's api from your laravel api
grant horwood
grant horwood

Posted on

accessing google's api from your laravel api

logging in using google via oauth is nothing new. in the laravel ecosystem, this is usually done using socialite for mvc applications. however, there are limits to this model, especially if you want to access google apis on behalf of the user, ie. to get information about a user's google drive documents in your application; or if you're building an api that is accessed by a separate web frontend.

in this walkthrough, we'll be modifying our existing laravel 8 restful(ish) api to accept logins via google and to read data about the user's google drive via the google api.

doing this requires us to ditch socialite and build the functionality using google's php sdk. this requires a bit more work and planning, but the results are worthwhile.

you should already have an api in laravel 8 and, ideally, a google account and project with oauth access credentials. if you do not have a google project like this already, or if you want to confirm that you've set it up correctly, there is a step-by-step overview on how to do this at the end of this walkthrough.

the flyover

in this walkthrough, we are going to:

  • install and configure passport to protect our api endpoints
  • install the google api php sdk
  • build the endpoints necessary to log in with google and issue a passport bearer token
  • call the google api to get information on the logged-in user
  • call the google api to get data on the user's google drive
  • supplementally, go through setting up a google project with ouath access in case we don't already have one

when we're done, we should be able to let people login with google, issue bearer tokens to let them access our api, and access users' read-only google drive data from our api.

what we're building

the flow of the project, when done, will be:

  • the frontend will call our api to get a google-hosted authorization url
  • the frontend will redirect to that url
  • the user will login into google
  • google will redirect to our frontend redirect page with an authorization code in the query string
  • our frontend will harvest that authorization code and call our api's 'login' endpoint with it
  • the api's login method will call the google api and send the authorization code. it will receive an access code and refresh token as a response. these will be stored.
  • the login method will call the google api for user data and create a user record if one doesn't already exist. it will then issue a bearer token using passport, returning it to the front end.
  • the frontend will use the bearer token for all subsequent calls to our api to authenticate the user
  • another api endpoint will use the user's stored access code to call the google api for user data. in this example, a list of files in the user's google drive.
  • if the google access code has expired, the api will use the stored refresh token for the user to get a new access code

note: we will not actually be building any of the frontend here. we will be using curl and some copy-pasting of urls into the browser to simulate the frontend.

what you should already have

you should already have:

  • a laravel 8 api
  • an operating system that is linux or similar enough
  • a google project with oauth access and the associated config.json. if you do not have one of these, instructions are given at the end of the walkthrough.

contents

install and configure passport

let's head over to our existing laravel 8 api.

we're going to use passport to issue and validate bearer tokens to protect endpoints in our api. let's start with installing

composer require laravel/passport
php artisan migrate
php artisan passport:install
Enter fullscreen mode Exit fullscreen mode

here we installed the passport composer package, ran the migrations to set up the tables that passport needs, and installed the necessary data to make it work.

once passport is installed, we need to register it with laravel as a guard for apis. we do this by editing the 'guards' section of config/auth.php to look like this:

in config/auth.php

    'guards' => [
        'api' => [
            // passport
            'driver' => 'passport',
            'provider' => 'users',
        ],      
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ],
Enter fullscreen mode Exit fullscreen mode

once we have passport installed and configured, we can use it to protect endpoints in our routes/api.php file like so:

Route::middleware(['auth:api'])->group(function () {
    Route::get('stuff', '\App\Http\Controllers\api\StuffController@getStuff');
});
Enter fullscreen mode Exit fullscreen mode

the route here isn't important. the important part is the middleware(['auth:api']). this middleware prevents any request that does not include a valid bearer token issued by passport from accessing the endpoint.

in order for our frontend to access endpoint, we would need our request to include the Authorization header with a good token, ie.

curl -X GET \
-H "Accept: application/json" \
-H "Authorization: Bearer <a good token here>" \
'http://api.ourproject.dev/api/stuff'
Enter fullscreen mode Exit fullscreen mode

of course, that leaves the question 'how do users actually get these tokens?'. well, traditionally that requires building a 'register' and 'login' endpoint that allows users to create accounts with an email and password and then log in with those credentials to get their bearer token. we won't be doing that.

instead, we will be building google login functionality with the googleapi php sdk.

install google's apiclient

to implement google login, we're going to need the googleapi php client. we can install this quickly with composer:

composer require google/apiclient
Enter fullscreen mode Exit fullscreen mode

as of this writing, the current version is 2.12.

the config.json

in order to access google apis, we are going to need to authenticate ourselves with google. this is done via the config.json file that google issued for your google project.

you should have a file from google that looks similar to this and it should be called config.json in the root of your laravel project.

{
  "web": {
    "client_id": "181003834811-qofn3o4rfovp89uieevr721mip9531ev.apps.googleusercontent.com",
    "project_id": "loginproject-354485",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "GOBTPF-R0aJTYdEtmKvz3OPjYu0JtrLCB20",
    "redirect_uris": [
      "https://fruitbat.studio/oauth.html"
    ],
    "javascript_origins": [
      "https://fruitbat.studio"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

set up a controller

next, we're going to create a controller where we will be putting all our methods for our google endpoints.

when we're done, this controller will have methods for endpoints that:

  • get the authorization url our front end needs for google login
  • accept google's auth code, validate it with google, create a user account for it in our api's database and issue a bearer token
  • get data on the user's google drive and return it

additionally, we will have two helper private methods:

  • a private method to build a client object for the google api
  • a private method to register the session user with the google client

that's in the future, though. for now we will make an empty controller that looks like:

<?php
namespace App\Http\Controllers\api;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller as Controller;

use App\Models\User;

/**
 * Google Controller
 *
 * INCOMPLETE
 */
class GoogleController extends Controller
{
}
Enter fullscreen mode Exit fullscreen mode

one thing we notice is that we are not useing any google packages here. the google api package is not namespaced (for whatever reason), so we will be accessing it directly going forward, ie by referring to \Google_Client.

build a google client

the first method we're going to put in our controller is a private one to build a google client. let's take a look at it.

    /**
     * Gets a google client
     *
     * @return \Google_Client
     * INCOMPLETE
     */
    private function getClient():\Google_Client
    {
        // load our config.json that contains our credentials for accessing google's api as a json string
        $configJson = base_path().'/config.json';

        // define an application name
        $applicationName = 'myfancyapp';

        // create the client
        $client = new \Google_Client();
        $client->setApplicationName($applicationName);
        $client->setAuthConfig($configJson);
        $client->setAccessType('offline'); // necessary for getting the refresh token
        $client->setApprovalPrompt ('force'); // necessary for getting the refresh token
        // scopes determine what google endpoints we can access. keep it simple for now.
        $client->setScopes(
            [
                \Google\Service\Oauth2::USERINFO_PROFILE,
                \Google\Service\Oauth2::USERINFO_EMAIL,
                \Google\Service\Oauth2::OPENID,
                \Google\Service\Drive::DRIVE_METADATA_READONLY // allows reading of google drive metadata
            ]
        );
        $client->setIncludeGrantedScopes(true);
        return $client;
    } // getClient
Enter fullscreen mode Exit fullscreen mode

we observe here that this method reads the json from our config.json file and then uses setAuthConfig() on our newly-made Google_Client to authenticate our client with google.

we also call setAccessType() with 'offline' and setApprovalPrompt() with 'force'. this is to indicate to the client that we are interacting with the api without any direct user interaction, ie. the user is 'offline'. it is necessary to do this if we want to get a 'refresh token' from google. we'll discuss more about refresh tokens later.

we also notice here that we call setScopes(). this informs the client of the subset of functionality that we intend to access from google.

return an authorization url

the first step in the login process is to create and return an authorization url to the front end. this is an url hosted by google where users are sent to login and accept our request for access to their data.

the frontend will present to the user a button saying something like 'login with google'. on clicking it, the frontend will call our endpoint to get the authorization url and then immediately redirect the browser to that url.

let's look at the method:

    /**
     * Return the url of the google auth.
     * FE should call this and then direct to this url.
     *
     * @return JsonResponse
     * INCOMPLETE
     */
    public function getAuthUrl(Request $request):JsonResponse
    {
        /**
         * Create google client
         */
        $client = $this->getClient();

        /**
         * Generate the url at google we redirect to
         */
        $authUrl = $client->createAuthUrl();

        /**
         * HTTP 200
         */
        return response()->json($authUrl, 200);
    } // getAuthUrl
Enter fullscreen mode Exit fullscreen mode

this method is only three lines long, but it does a fair bit.

the first line calls the private method getClient() that we created previously. this builds a google access client that is authorized with our credentials and returns it.

the second line calls google client's createAuthUrl() method which generates the url we want the frontend to redirect to. finally, we return the url along with HTTP 200.

next, we will hook up this method to an endpoint in our routes/api.php

Route::get('google/login/url', '\App\Http\Controllers\api\GoogleController@getAuthUrl');
Enter fullscreen mode Exit fullscreen mode

test the authorization url

once we have our endpoint pointing to our controller method, we can simulate a frontend using curl like so:

curl -X GET \
-H "Accept: application/json" \
'http://api.ourproject.dev/api/google/login/url'
Enter fullscreen mode Exit fullscreen mode

notice in this curl we explicitly state the http method with -X GET, tell the api that we expect json return with out Accept header, and pass the url of the endpoint.

our response will be similar to:

https://accounts.google.com/o/oauth2/auth?response_type=code&access_type=offline&client_id=181003834811-qofn3o4rfovp89uieevr721mip9531ev.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Ffruitbat.studio%2Foauth&state&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20openid&approval_prompt=force&include_granted_scopes=true
Enter fullscreen mode Exit fullscreen mode

that's the url we want our frontend to redirect to. to test it out, we will copy and paste it (the one you received from your curl call, not the one shown above!) into a browser.

we should see the familiar google login page.

screen capture of the google login screen for oauth
the familiar google login screen

if you log in to google on this page, you will be automatically redirected to the url you specified as the redirect_uri in your config.json. there will be a lot of stuff on the query string

https://fruitbat.studio/?code=4%2F0AX4XfWisBNVvCTCgMOXK-sB69E7WeAK3i6Jt_pcfQeJyMs4l2IOgvjDzNUbAKHsMWa88ng&scope=email%20profile%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20openid&authuser=2&hd=fruitbat.studio&prompt=consent
Enter fullscreen mode Exit fullscreen mode

the important thing here is value for code on the query string. in the above example it is:

4%2F0AX4XfWisBNVvCTCgMOXK-sB69E7WeAK3i6Jt_pcfQeJyMs4l2IOgvjDzNUbAKHsMWa88ng
Enter fullscreen mode Exit fullscreen mode

this is our 'authorization code'. we will be calling our (not yet built) login endpoint and passing this code to it.

build a login endpoint

now we want to build an endpoint that will take that authorization code from google and exchange it for an access token, validating the code in the process.

migrations and model updates for login

before we can write login, however, we are going to need to add some fields to our User model, namely:

  • provider_name this is the name of the service we logged in with. in this case it will always be 'google', but if we decide to add a feature to log in with facebook in the future, we will need this column to differentiate.
  • provider_id the unique id of the user according to google.
  • google_access_token_json the json that google returns when we trade the auth code for the access token. this includes the access token, but also the refresh token and some other pieces of data.

create a new migration for your project to alter the users table and add this:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AlterTableUsersAddSocialite extends Migration
{

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function ($table) {

            $table->string('name')->nullable()->change();
            $table->string('email')->nullable()->change();
            $table->string('password')->nullable()->change();
            $table->string('provider_name')->nullable()->after('password');
            $table->string('provider_id')->nullable()->after('password');
            $table->text('google_access_token_json')->nullable()->after('provider_name');

        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function ($table) {
            $table->dropColumn('provider_id');
            $table->dropColumn('provider_name');
            $table->dropColumn('google_access_token_json');
            $table->string('name')->change();
            $table->string('email')->unique()->change();
            $table->string('password')->change();
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

we observe that in this migration we not only added the columns needed to identify our user from google, but that we also made the name, email and password nullable(). this is because we are not guaranteed to get either a name or email back from google and we certainly won't be getting a password.

before we can run this migration, we need to install dbal to allow column changes:

composer require doctrine/dbal
Enter fullscreen mode Exit fullscreen mode

and now we are good to run our migrations!

php artisan migrate:fresh
Enter fullscreen mode Exit fullscreen mode

once the migrations are done, we need to update our User model to allow mass filling of our new columns. open app/models/User.php and update the $fillable array:

    /**
     * The attributes that are mass assignable.
     *
     * @var array<string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'provider_id',
        'provider_name',
        'google_access_token_json',
    ];
Enter fullscreen mode Exit fullscreen mode

write the login method

now we are ready to write our login method.

in the GoogleController we made before, add this:

    /**
     * Login and register
     * Gets registration data by calling google Oauth2 service
     *
     * @return JsonResponse
     */
    public function postLogin(Request $request):JsonResponse
    {

        /**
         * Get authcode from the query string
         * Url decode if necessary
         */
        $authCode = urldecode($request->input('auth_code'));

        /**
         * Google client
         */
        $client = $this->getClient();

        /**
         * Exchange auth code for access token
         * Note: if we set 'access type' to 'force' and our access is 'offline', we get a refresh token. we want that.
         */
        $accessToken = $client->fetchAccessTokenWithAuthCode($authCode);

        /**
         * Set the access token with google. nb json
         */
        $client->setAccessToken(json_encode($accessToken));

        /**
         * Get user's data from google
         */
        $service = new \Google\Service\Oauth2($client);
        $userFromGoogle = $service->userinfo->get();

        /**
         * Select user if already exists
         */
        $user = User::where('provider_name', '=', 'google')
            ->where('provider_id', '=', $userFromGoogle->id)
            ->first();

        /**
         */
        if (!$user) {
            $user = User::create([
                    'provider_id' => $userFromGoogle->id,
                    'provider_name' => 'google',
                    'google_access_token_json' => json_encode($accessToken),
                    'name' => $userFromGoogle->name,
                    'email' => $userFromGoogle->email,
                    //'avatar' => $providerUser->picture, // in case you have an avatar and want to use google's
                ]);
        }
        /**
         * Save new access token for existing user
         */
        else {
            $user->google_access_token_json = json_encode($accessToken);
            $user->save();
        }

        /**
         * Log in and return token
         * HTTP 201
         */
        $token = $user->createToken("Google")->accessToken;
        return response()->json($token, 201);
    } // postLogin
Enter fullscreen mode Exit fullscreen mode

the high-level view of what this method does is:

  • acquire the google auth code from the query string
  • create a google api client
  • call the google api to exchange the auth code for an access code by using fetchAccessTokenWithAuthCode()
  • call setAccessToken() with the returned token so the google client knows what user it's acting on behalf of
  • use the google clients Oauth2 service to call google and get the user's data. this data includes google's user id for the user. we call it the 'provider_id'.
  • try to select the user from our database using that 'provider_id' and the 'provider_name' 'google'.
  • if the user does not exist in our db, create the record, storing the whole json object of the access code
  • if the user does exist, update the access code jason in the db
  • use passport to create a bearer token that the frontend will use to access our endpoints going forward
  • return the bearer token to the frontend

we need to hook up this method to an endpoint, so we add this to routes/api.php. note that we're using POST here.

Route::post('google/auth/login', '\App\Http\Controllers\api\GoogleController@postLogin');
Enter fullscreen mode Exit fullscreen mode

and now we have a full end-to-end login solution. let's test it.

test the login

we're going to do a fast test of this login by simulating a frontend using curl and a browser.

the login flow, we remember, is:

  • request an auth url from the api
  • redirect to that auth url
  • catch the redirect back from google and harvest the code value from the query string
  • send that auth code to the api's login endpoint
  • get a valid bearer token back

so, let's do that!

first, we call the api to get the auth url. the curl to do that is straightforward, just replace the domain with your own

curl -X GET \
-H "Accept: application/json" \
'http://kill.bintracker.test/api/google/login/url'
Enter fullscreen mode Exit fullscreen mode

this curl call, when run, will return us an url. if we copy and past that into our browser location bar, we will be taken to the google login page.

after logging in with google, we will be redirected back to the redirect_uri we set in our config.json. we should be able to see a value on the query string of the url after code=. we copy that.

we then use this curl call to hit the login endpoint, adding the code we copied on the query string above

curl -s -X POST \
-H 'Accept: application/json' \
http://kill.bintracker.test/api/google/auth/login?auth_code=<the auth code>
Enter fullscreen mode Exit fullscreen mode

and we should get back a bearer token that we can use to access endpoints guarded by laravel's auth:api middleware. success.

important note: auth codes from google have a very short lifespan. if you try to get an access token with an expired auth code, google will error (somewhat cryptically). be speedy!

all about refresh tokens

before we continue with accessing our users' google drives from our api, we should take a moment to talk about refresh tokens.

let's take a look at the access token we got back from google in our login method.

{
  "access_token": "yb89.A1ARrdaN-TUhO6O2n37sS8fb9lkUy2qEZ_sEYmAZDu8Pdlrj9BVyK-5J-k6grcjGZp4R5mNOvNZ9XsdFo30rMpaheyIdWT8a6GTsflPtNSTRiFaX23dRGvoZHFKa6OKxSi_cfWXQQJFHNx3jnmZac_4NvvnF3g",
  "expires_in": 3599,
  "refresh_token": "1//03i8ihVroCNF9CgYIARHAGHYSNvG-L9IrMLmBniN0vWUu5-CCuDrXHfCNSTVxfGvRkze21nlVDzNUlsgk9fV6AUgDOPbEz4A_lxug",
  "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email openid",
  "token_type": "Bearer",
  "id_token": "eyJhbGcjOiJTYzI1NiIsImtpZCI6IjV3YjQyOTY2MmRiMDc4NmYyZWZlZmUxM2MxZWIxMmEyOGRjNDQyZDAiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxNzExMDM5Mzk4NjEtcW9mbjNvNHJmb3ZwODl1aWVldnI3MjFtaXA5NTMxZXYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIxNzExMDM5Mzk4NjEtcW9mbjNvNHJmb3ZwODl1aWVldnI3MjFtaXA5NTMxZXYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTQ2NjI3Njc3MTUzODYyMTE0NDUiLCJoZCI6ImNsb3ZlcmhpdGNoLmNhIiwiZW1haWwiOiJnaG9yd29vZEBjbG92ZXJoaXRjaC5jYSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiQkxTLTIyQ1NkTUJzYXNIU0lBeWZYdyIsIm5hbWUiOiJHcmFudCBIb3J3b29kIiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hLS9BT2gxNEdnMnB1b0lmclJNMlBsSGY0cVJGU0h2bkExR2RUZkptWThxVUk2Qj1zOTYtYyIsImdpdmVuX25hbWUiOiJHcmFudCIsImZhbWlseV9uYW1lIjoiSG9yd29vZCIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNjQ4MjUzMjEwLCJleHAiOjE2NDgyNTY3MTC0.FDvOSkeU6TO9gM6jU1rn2JUBUn5Ch1w_L9OeQjk-dnRW5ANnE-c9exZmJyAzRIgYfL9-zx-CVCSGXb0cevk71FVopcEnv7isldA0t2PuVUatt4MpO3lRN5qXqiKI15Xp_YyIEtUSuNbkBjrqiPGyK3_aopyRcDlk8pPW-3fEXSWktEjHnvdyfJr8XjInaW1VD77WXTxfMmOp9BcU2XC3YDC0zmYxTXDrHxUmnybNWQLLf9o6qDhocMC8xo4xoNvNG67ooQEWwIoycfpJzn1L7_87IfZTAIZn9maRhSIKbZF3aeHeKUwmUiLvAYl9SfGa0SjA0pfJ_betnfKVqnS0Vg",
  "created": 1648253210
}
Enter fullscreen mode Exit fullscreen mode

although it's called an 'access token', what it really is is a big json object that contains an access token. there's other stuff here, too, most notably an expires_in timestamp and a refresh_token.

google access tokens are only valid for one hour. that's what the expires_in of 3599 seconds means. after that, if we wish to continue calling the google api, we either need to send the user back to google to login again, which is a terrible user experience, or call google to get a new, valid access token. we're going to do the second thing.

to get a new access token, we request one from google using the refresh_token we have. in order to make sure this process runs smoothly, whenever we call the google api we first check if the access token is expired and, if so, we request a new one before proceeding. we will be doing that in the next step when we call google for our user's drive information.

note: refresh tokens are normally very long-lived. the exception is if your google app is not 'published', in which case they typically last only seven days. if you find your refresh token is timing out, consider publishing your app.

google drive access (at last)

we're now going to build the endpoint that calls google on the user's behalf and gets information about their google drive.

write the user client helper method

before we can start that, though, we're going to need to create a helper function that gets a google client that is set to our session user. earlier, we wrote a getClient() private function that built and returned a google client. now what we need is a function that wraps getClient() and 'logs in' our user using their google access token that we have stored in the database.

we'll put this function in our controller. let's look at it:

    /**
     * Returns a google client that is logged into the current user
     *
     * @return \Google_Client
     */
    private function getUserClient():\Google_Client
    {
        /**
         * Get Logged in user
         */
        $user = User::where('id', '=', auth()->guard('api')->user()->id)->first();

        /**
         * Strip slashes from the access token json
         * if you don't strip mysql's escaping, everything will seem to work
         * but you will not get a new access token from your refresh token
         */
        $accessTokenJson = stripslashes($user->google_access_token_json);

        /**
         * Get client and set access token
         */
        $client = $this->getClient();
        $client->setAccessToken($accessTokenJson);

        /**
         * Handle refresh
         */
        if ($client->isAccessTokenExpired()) {
            // fetch new access token
            $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
            $client->setAccessToken($client->getAccessToken());

            // save new access token
            $user->google_access_token_json = json_encode($client->getAccessToken());
            $user->save();
        }

        return $client;
    } // getUserClient
Enter fullscreen mode Exit fullscreen mode

the first thing we do in this function is get the User object for the user that called our api endpoint. this record includes the google access token for this user. note that we need to call stripslashes() on the access token json. mysql is keen to add escape chars, and if we do not remove them, google will error with unhelpful messages when we try to proceed.

next, we build a google access client and call setAccessToken() on it with the access token json object for the user. this, in essence, 'logs in' our user with google so we can make calls to the google api to access their resources, like their drive.

then we need to determine if the access token has expired and take the necessary steps. above, we discussed refresh tokens and how we can use them to get new access tokens if needed. this is where we do all that stuff.

we start by testing if the access token is expired using isAccessTokenExpired(). if it is not, the client is good to use and we can proceed. if the access token is no longer valid, we need to fetch a new access token by calling fetchAccessTokenWithRefreshToken(). this method returns a whole new access token json object from google. we assign this new token to the google client using setAccessToken(), in essence trying the 'log in to google' step again, and then save the new token to the database for the user.

finally, we return our client object.

write the getDrive endpoint

we now have all the pieces needed to write the endpoint that returns the metadata on our user's google drive.

let's take a look at it.

    /**
     * Get meta data on a page of files in user's google drive
     *
     * @return JsonResponse
     */
    public function getDrive(Request $request):JsonResponse
    {
        /**
         * Get google api client for session user
         */
        $client = $this->getUserClient();

        /**
         * Create a service using the client
         * @see vendor/google/apiclient-services/src/
         */
        $service = new \Google\Service\Drive($client);

        /**
         * The arguments that we pass to the google api call
         */
        $parameters = [
            'pageSize' => 10,
        ];

        /**
         * Call google api to get a list of files in the drive
         */
        $results = $service->files->listFiles($parameters);

        /**
         * HTTP 200
         */
        return response()->json($results, 200);
    }
Enter fullscreen mode Exit fullscreen mode

the first thing we do here is build a google client and register the user with it using our newly-created getUserClient() method. we then use that client to build a service object.

service objects are how we interact with the google api; there is one for each category of apis google offers. if you look in vendor/google/apiclient-services/src/ you will see a list of them. for this example, we are using the Drive service.

we then set up the parameters we want to pass to the call; in this example we are keeping it simple with just a basic pagination argument.

finally, we call $service->files->listFiles() to actually make the request to the google api. we harvest and return the result.

all that remains is to hook up our new method to an endpoint in the routes/api.php directory like so:

Route::middleware(['auth:api'])->group(function () {
    Route::get('google/drive', '\App\Http\Controllers\api\GoogleController@getDrive');
});
Enter fullscreen mode Exit fullscreen mode

putting it all together

now that we have all the parts for our project, let's see what it looks like put together.

the GoogleController file

<?php
namespace App\Http\Controllers\api;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller as Controller;

use App\Models\User;

/**
 * Google Controller
 *
 */
class GoogleController extends Controller
{
    /**
     * Return the url of the google auth.
     * FE should call this and then direct to this url.
     *
     * @return JsonResponse
     */
    public function getAuthUrl(Request $request):JsonResponse
    {
        /**
         * Create google client
         */
        $client = $this->getClient();

        /**
         * Generate the url at google we redirect to
         */
        $authUrl = $client->createAuthUrl();

        /**
         * HTTP 200
         */
        return response()->json($authUrl, 200);
    } // getAuthUrl


    /**
     * Login and register
     * Gets registration data by calling google Oauth2 service
     *
     * @return JsonResponse
     */
    public function postLogin(Request $request):JsonResponse
    {

        /**
         * Get authcode from the query string
         */
        $authCode = urldecode($request->input('auth_code'));

        /**
         * Google client
         */
        $client = $this->getClient();

        /**
         * Exchange auth code for access token
         * Note: if we set 'access type' to 'force' and our access is 'offline', we get a refresh token. we want that.
         */
        $accessToken = $client->fetchAccessTokenWithAuthCode($authCode);

        /**
         * Set the access token with google. nb json
         */
        $client->setAccessToken(json_encode($accessToken));

        /**
         * Get user's data from google
         */
        $service = new \Google\Service\Oauth2($client);
        $userFromGoogle = $service->userinfo->get();

        /**
         * Select user if already exists
         */
        $user = User::where('provider_name', '=', 'google')
            ->where('provider_id', '=', $userFromGoogle->id)
            ->first();

        /**
         */
        if (!$user) {
            $user = User::create([
                    'provider_id' => $userFromGoogle->id,
                    'provider_name' => 'google',
                    'google_access_token_json' => json_encode($accessToken),
                    'name' => $userFromGoogle->name,
                    'email' => $userFromGoogle->email,
                    'role_id' => 2,
                    //'avatar' => $providerUser->picture, // in case you have an avatar and want to use google's
                ]);
        }
        /**
         * Save new access token for existing user
         */
        else {
            $user->google_access_token_json = json_encode($accessToken);
            $user->save();
        }

        /**
         * Log in and return token
         * HTTP 201
         */
        $token = $user->createToken("Google")->accessToken;
        return response()->json($token, 201);
    } // postLogin


    /**
     * Get meta data on a page of files in user's google drive
     *
     * @return JsonResponse
     */
    public function getDrive(Request $request):JsonResponse
    {
        /**
         * Get google api client for session user
         */
        $client = $this->getUserClient();

        /**
         * Create a service using the client
         * @see vendor/google/apiclient-services/src/
         */
        $service = new \Google\Service\Drive($client);

        /**
         * The arguments that we pass to the google api call
         */
        $parameters = [
            'pageSize' => 10,
        ];

        /**
         * Call google api to get a list of files in the drive
         */
        $results = $service->files->listFiles($parameters);

        /**
         * HTTP 200
         */
        return response()->json($results, 200);
    }


    /**
     * Gets a google client
     *
     * @return \Google_Client
     */
    private function getClient():\Google_Client
    {
        // load our config.json that contains our credentials for accessing google's api as a json string
        $configJson = base_path().'/config.json';

        // define an application name
        $applicationName = 'myfancyapp';

        // create the client
        $client = new \Google_Client();
        $client->setApplicationName($applicationName);
        $client->setAuthConfig($configJson);
        $client->setAccessType('offline'); // necessary for getting the refresh token
        $client->setApprovalPrompt('force'); // necessary for getting the refresh token
        // scopes determine what google endpoints we can access. keep it simple for now.
        $client->setScopes(
            [
                \Google\Service\Oauth2::USERINFO_PROFILE,
                \Google\Service\Oauth2::USERINFO_EMAIL,
                \Google\Service\Oauth2::OPENID,
                \Google\Service\Drive::DRIVE_METADATA_READONLY
            ]
        );
        $client->setIncludeGrantedScopes(true);
        return $client;
    } // getClient


    /**
     * Returns a google client that is logged into the current user
     *
     * @return \Google_Client
     */
    private function getUserClient():\Google_Client
    {
        /**
         * Get Logged in user
         */
        $user = User::where('id', '=', auth()->guard('api')->user()->id)->first();

        /**
         * Strip slashes from the access token json
         * if you don't strip mysql's escaping, everything will seem to work
         * but you will not get a new access token from your refresh token
         */
        $accessTokenJson = stripslashes($user->google_access_token_json);

        /**
         * Get client and set access token
         */
        $client = $this->getClient();
        $client->setAccessToken($accessTokenJson);

        /**
         * Handle refresh
         */
        if ($client->isAccessTokenExpired()) {
            // fetch new access token
            $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
            $client->setAccessToken($client->getAccessToken());

            // save new access token
            $user->google_access_token_json = json_encode($client->getAccessToken());
            $user->save();
        }

        return $client;
    } // getUserClient
}
Enter fullscreen mode Exit fullscreen mode

and the routes

Route::get('google/login/url', '\App\Http\Controllers\api\GoogleController@getAuthUrl');
Route::post('google/auth/login', '\App\Http\Controllers\api\GoogleController@postLogin');
Route::middleware(['auth:api'])->group(function () {
    Route::get('google/drive', '\App\Http\Controllers\api\GoogleController@getDrive');
});
Enter fullscreen mode Exit fullscreen mode

of course, going forward, there are a lot of changes, expansions and improvements that we can make to this project. we can migrate the client functionality to a trait, for instance, or expand the login functionality to handle email/pass registration or other social logins. of course we can also access different, more relevant google apis. the objective of this walkthrough is only to provide the starting point from which to build better, more useful things.

setting up some google api credentials

if we don't already have a google project with oauth credentials and some scopes set, no worries, setting it up is a not-too-difficult process.

let's assume we already have a google account for gmail or whatever (because, realistically, who doesn't). we can use that account to create what google calls a 'project' and then have that project issue some access credentials.

the ultimate objective here is to get a json-formatted config file from google that we can use to authorize our api requests. we start by making a google project.

create a google project

to create a project, head over to the developer console:

https://console.developers.google.com/

once there, we create a new project by clicking 'Select a project' and then, in the modal, 'new project'.

"screen capture of creating a new project on the google developers console"
clicking 'new project' will... create a new project

all we need for our new project is a name. there is the option for 'location', but we will ignore that for now. we're calling this project 'loginproject', because we lack originality.

"screen capture of naming your new google project"
like pets, projects should have names

now that we have our project created, we need to tell google which apis we intend to use. there are lots.

click the menu item for 'enable apis and services'

"screen capture of the 'enable apis' button on the google new project page"
apis need enabling, se we shall enable them

and then scroll down until you see the big button for 'Google Drive API'. click it.

"screen capture of the button for the google drive api"
click the apis you want. don't be greedy.

then all we need to do is click the big blue 'enable' button we are presented with.

"screen capture of the enable api confirm button"

this takes a surprisingly long time.

set up oauth scopes and consent screen

now we're at our project's home screen. if you look, you will see that there is a notice saying "To use this API, you may need credentials. Click 'CREATE CREDENTIALS' to get started."

we're not going to do that.

since we want to access the google api on behalf of other users, what we really need is oauth2 access, and to do that we need an oauth consent screen and some selected scopes.

oauth is a huge topic, which we won't be covering in detail here. the tl;dr version is that oauth2 allows a user with a google account (or any other similar service, ie facebook or github) to give our project limited access to services on their behalf with their permission. what we're doing here is selecting the scopes that define that 'limited access' and putting in the information for the screen that google shows the user to get their permission.

to start we click the menu selection the left hand side called 'Oauth Consent Screen'

"screen capture of the menu where we click the option to bild the oauth screen"
don't click 'credentials' just yet

google asks us if we want this to be 'internal' or 'external'. since we want anyone with a google account to login to our api, the choice is 'external'. select that and click the 'create' button.

"screen capture of the radio button to choose 'external' as your user type"
the public is, by definition, 'external'

we are now presented with a form where we enter data about our oauth confirmation screen. this is for the page that our frontend will redirect us to for confirmation with google before logging us in. there's some important stuff here, namely:

App Name: this can be anything. we're going with 'logintestapp' because it's catchy.

User support email: you can enter any email address you have access to here

App Domain: this is for building links on the authorization page that users can click on. they don't need to be real pages for development, but they must be for when you go live. the domain of these urls must be one of the domains that you enter in the next section 'Authorised Domains'

Authorised Domains: this is the domain of your project and must go to a page you control. just enter the domain here.

click 'Save and Continue'

"screen capture of the form to fill out for our oauth authorization screen"
remember. this data needs to be good before you go live

now we need to add some 'scopes'. basically, this just tells google what access we want to a user's resources. we register this with google so they can inform users what powers we want before agreeing to log into our project. informed decisions are good! click the 'add or remove scopes' button.

"screen capture of the 'add or remove scopes' button"
let's add some scopes

a panel will slide out on the right that shows all the possible scopes we can choose. there are a lot. remember when we added the google drive api to our project? the scopes for that are included here. hit the right page arrow until you see 'drive.metadata.readonly' and 'drive.readonly' and select those.

"screen capture of the interface to select scopes"
there are many pages of scopes. keep scrolling.

once we've selected our scopes and clicked the 'update' button, we will see them listed under 'your sensetive scopes'.

click 'save and continue'.

we now need to add test users. while in test mode, only test users can authenticate, so these test users must be google accounts we have access to.

click the 'add users' button and, in the right panel, add the email addresses of the test users we want to use. we have a maximum of 100.

when we have our test users entered, click 'save and continue'.

"screen capture of the interface for choosing test user"
more test users are better

and that's our conent screen made and our oauth scopes selected. click the 'back to dashboard' button.

getting our credentials

now that we have our oauth set up, it's time to get our credentials so we can call the google api and they know who we are. on the left hand menu click the 'credentials' link.

"screen capture of the menu to click the 'credentials' option"
now we can click 'credentials'

from the 'create credentials' menu at the top, we select 'Oauth Client ID'.

"screen capture of the button to get 'oauth credentials'"
we're doig oauth, so click 'oauth'

we are then asked to select the 'application type'. we're going to choose 'web application' here, even though we are building an api, since our frontend is going to be web delivered.

the revealed form has a few more fields. 'Name' is obvious. 'Authorized JavaScript Origins' less so. this sets the uris that google will accept api calls from when they are made by javascript. since we are calling the google api from our api, this is of less concern. fill this in with something appropriate.

lastly, we fill in the 'Authorized Redirect Uris'. these are the uris that our oauth authorization screen will redirect to after the user signs into google. this will need to be the uri on your frontend that calls your api to complete the login process. you should need only one.

lastly, we hit 'create'

"screen capture of the form to fill out to create oauth credentials"

we are presented with a modal that shows us our credentials! success!

what we want from this screen most of all is the json file. hit that 'download json' link and save the resulting config.json file someplace safe.

the config.json file is what our google api library will load to authenticate our api with google and allow us to log user's in and access their google drives on their behalf.

"screen capture of the button to download the completed config.json"
at last, our config.json file

here's a sample of the config.json

{
  "web": {
    "client_id": "181003834811-qofn3o4rfovp89uieevr721mip9531ev.apps.googleusercontent.com",
    "project_id": "loginproject-354485",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "GOBTPF-R0aJTYdEtmKvz3OPjYu0JtrLCB20",
    "redirect_uris": [
      "https://fruitbat.studio/oauth.html"
    ],
    "javascript_origins": [
      "https://fruitbat.studio"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

we can see here that we have both a client_id and client_secret for authenticating our project with google. we also have an entry in redirect_uris to tell google's oauth authentication screen where to redirect to on success. this is all valuable information and we should keep it safe and not commit it to our repository.

Top comments (1)

Collapse
 
abdullmng profile image
abdullmng

Very helpful, Please make that of Facebook as an extension of this