In this tutorial, we’ll be using Google Drive REST API with Laravel application. We’ll authenticate the user with OAuth service and retrieve their access token. We’ll then use it to manage their drive data and perform read and write operations on their behalf.

Create a new project and generate authentication scaffolding. Install Socialite and Google Client Library. We’ll use Socialite to authenticate the user with Google’s OAuth service.

composer create-project laravel/laravel laravel-drive --prefer-dist composer require laravel/socialite composer require google/apiclient cd laravel-drive php artisan make:auth 1 2 3 4 5 composer create - project laravel / laravel laravel - drive -- prefer - dist composer require laravel / socialite composer require google / apiclient cd laravel - drive php artisan make : auth

Create Google Drive Application

Now head over to Google Developers Console and create or select an existing project. You can use this wizard to register Google Drive Application.

Now click continue and go to credentials.

Choose Google drive API, select Web Server and check User Data.

Enter client ID name and enter Javascript origin and redirect URIs. Enter your domain name instead of localhost address for production. Redirect URI will be used to redirect users back to your app after authentication. Create client ID and enter product name. Download JSON credentials file and save it.

Now click on Enable API and Services from the dashboard.

Search for Google Drive and Google+ APIs and make sure both are enabled.

Configure Google Service

Now edit config/serivces.php and add google service at the end of services array.

'google' => [ 'client_id' => env('GOOGLE_CLIENT_ID'), 'project_id' => env('GOOGLE_APP_ID'), 'auth_uri' => 'https://accounts.google.com/o/oauth2/auth', 'token_uri' => 'https://accounts.google.com/o/oauth2/token', 'auth_provider_x509_cert_url' => 'https://www.googleapis.com/oauth2/v1/certs', 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 'redirect' => env('GOOGLE_REDIRECT'), 'redirect_uris' => [env('GOOGLE_REDIRECT')], ] 1 2 3 4 5 6 7 8 9 10 'google' = > [ 'client_id' = > env ( 'GOOGLE_CLIENT_ID' ) , 'project_id' = > env ( 'GOOGLE_APP_ID' ) , 'auth_uri' = > 'https://accounts.google.com/o/oauth2/auth' , 'token_uri' = > 'https://accounts.google.com/o/oauth2/token' , 'auth_provider_x509_cert_url' = > 'https://www.googleapis.com/oauth2/v1/certs' , 'client_secret' = > env ( 'GOOGLE_CLIENT_SECRET' ) , 'redirect' = > env ( 'GOOGLE_REDIRECT' ) , 'redirect_uris' = > [ env ( 'GOOGLE_REDIRECT' ) ] , ]

Now define enviroment variables in .env file.

GOOGLE_APP_ID=project-id GOOGLE_CLIENT_ID=YOUR_CLIENT_ID GOOGLE_CLIENT_SECRET=YOUR_CLIENT_SECRET GOOGLE_REDIRECT='http://127.0.0.1:3000/login/google/callback' 1 2 3 4 GOOGLE_APP_ID = project - id GOOGLE_CLIENT_ID = YOUR_CLIENT_ID GOOGLE_CLIENT_SECRET = YOUR_CLIENT_SECRET GOOGLE_REDIRECT = 'http://127.0.0.1:3000/login/google/callback'

You Redirect URI must match with the one we entered during application setup.

OAuth Authentication

Add these routes to your app/routes/web.php file.

Route::get('/login/google', 'Auth\LoginController@redirectToGoogleProvider'); Route::get('login/google/callback', 'Auth\LoginController@handleProviderGoogleCallback'); 1 2 3 Route :: get ( '/login/google' , 'Auth\LoginController@redirectToGoogleProvider' ) ; Route :: get ( 'login/google/callback' , 'Auth\LoginController@handleProviderGoogleCallback' ) ;

Now define both methods in LoginController.

public function redirectToGoogleProvider() { $parameters = ['access_type' => 'offline']; return Socialite::driver('google')->scopes(["https://www.googleapis.com/auth/drive"])->with($parameters)->redirect(); } public function handleProviderGoogleCallback() { $auth_user = Socialite::driver('google')->user(); $user = User::updateOrCreate(['email' => $auth_user->email], ['refresh_token' => $auth_user->token, 'name' => $auth_user->name]); Auth::login($user, true); return redirect()->to('/'); // Redirect to a secure page } 1 2 3 4 5 6 7 8 9 10 11 12 13 public function redirectToGoogleProvider ( ) { $parameters = [ 'access_type' = > 'offline' ] ; return Socialite:: driver ( 'google' ) -> scopes ( [ "https://www.googleapis.com/auth/drive" ] ) -> with ( $parameters ) -> redirect ( ) ; } public function handleProviderGoogleCallback ( ) { $auth_user = Socialite:: driver ( 'google' ) -> user ( ) ; $user = User:: updateOrCreate ( [ 'email' = > $auth_user -> email ] , [ 'refresh_token' = > $auth_user -> token , 'name' = > $auth_user -> name ] ) ; Auth:: login ( $user , true ) ; return redirect ( ) -> to ( '/' ) ; // Redirect to a secure page }

We’re using “https://www.googleapis.com/auth/drive” scope to request user permissions for managing their drive data in redirectToGoogleProvider() method. In handleProviderGoogleCallback() method, we’re creating a new user if it doesn’t exist. if it exists, we’re updating their refresh_token. Make sure you add this column to your user’s table by modifying user migration and setting the password to nullable.

public function up() { Schema::create('users', function (Blueprint $table){ $table->increments('id'); $table->string('name'); $table->string('email')->unique(); $table->string('password')->nullable(); $table->string('refresh_token'); $table->rememberToken(); $table->timestamps(); }); } 1 2 3 4 5 6 7 8 9 10 11 12 public function up ( ) { Schema:: create ( 'users' , function ( Blueprint $table ) { $table -> increments ( 'id' ) ; $table -> string ( 'name' ) ; $table -> string ( 'email' ) -> unique ( ) ; $table -> string ( 'password' ) -> nullable ( ) ; $table -> string ( 'refresh_token' ) ; $table -> rememberToken ( ) ; $table -> timestamps ( ) ; } ) ; }

Dependency Injection

Create GoogleServiceProvider. We’ll inject Google Client class into service container to avoid unnecessary instantiation.

<?php namespace App\Providers; use Google_Client; use Illuminate\Support\Facades\Storage; use Illuminate\Support\ServiceProvider; class GoogleClientProvider extends ServiceProvider { public function boot() { } public function register() { $this->app->singleton(Google_Client::class, function ($app) { $client = new Google_Client(); Storage::disk('local')->put('client_secret.json', json_encode([ 'web' => config('services.google') ])); $client->setAuthConfig(Storage::path('client_secret.json')); return $client; }); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?php namespace App \ Providers ; use Google_Client ; use Illuminate \ Support \ Facades \ Storage ; use Illuminate \ Support \ ServiceProvider ; class GoogleClientProvider extends ServiceProvider { public function boot ( ) { } public function register ( ) { $this -> app -> singleton ( Google_Client:: class , function ( $app ) { $client = new Google_Client ( ) ; Storage:: disk ( 'local' ) -> put ( 'client_secret.json' , json_encode ( [ 'web' = > config ( 'services.google' ) ] ) ) ; $client -> setAuthConfig ( Storage:: path ( 'client_secret.json' ) ) ; return $client ; } ) ; } }

After instantiation of Google_Client class, we’re reading google service credentials and creating a temporary file with it. We’re doing this because setAuthConfig() method expects JSON file with credentials that we downloaded after setting up drive app. Now every time we type hint Google_Client in our methods, we’ll have access to this instance from the service container.

If you’re running Laravel 5.5 or above, you don’t have to add GoogleServiceProvider to the providers array in config/app.php. Laravel Auto-Discovery automatically register your service providers. However, if you’re using an order Laravel version, you are required to add it manually. Register GoogleServiceProvider by adding this line to the $providers array in config/app.php file.

'providers' => [ ...// other providers App\Providers\GoogleServiceProvider::class, ]; 1 2 3 4 'providers' = > [ . . . // other providers App \ Providers \ GoogleServiceProvider:: class , ] ;

Routes and Controller

Route::get('/drive', 'DriveController@getDrive'); // retreive folders Route::get('/drive/upload', 'DriveController@uploadFile'); // File upload form Route::post('/drive/upload', 'DriveController@uploadFile'); // Upload file to Drive from Form Route::get('/drive/create', 'DriveController@create'); // Upload file to Drive from Storage Route::get('/drive/delete/{id}', 'DriveController@deleteFile'); // Delete file or folder 1 2 3 4 5 6 7 8 9 Route:: get ( '/drive' , 'DriveController@getDrive' ) ; // retreive folders Route:: get ( '/drive/upload' , 'DriveController@uploadFile' ) ; // File upload form Route:: post ( '/drive/upload' , 'DriveController@uploadFile' ) ; // Upload file to Drive from Form Route:: get ( '/drive/create' , 'DriveController@create' ) ; // Upload file to Drive from Storage Route:: get ( '/drive/delete/{id}' , 'DriveController@deleteFile' ) ; // Delete file or folder

Add these routes to your app. Create DriveController and implement these methods to handle requests.

class DriveController extends Controller { private $drive; public function __construct(Google_Client $client) { $this->middleware(function ($request, $next) use ($client) { $client->refreshToken(Auth::user()->refresh_token); $this->drive = new Google_Service_Drive($client); return $next($request); }); } public function getDrive(){ $this->ListFolders('root'); } public function ListFolders($id){ $query = "mimeType='application/vnd.google-apps.folder' and '".$id."' in parents and trashed=false"; $optParams = [ 'fields' => 'files(id, name)', 'q' => $query ]; $results = $this->drive->files->listFiles($optParams); if (count($results->getFiles()) == 0) { print "No files found.

"; } else { print "Files:

"; foreach ($results->getFiles() as $file) { dump($file->getName(), $file->getID()); } } } function uploadFile(Request $request){ if($request->isMethod('GET')){ return view('upload'); }else{ $this->createFile($request->file('file')); } } function createStorageFile($storage_path){ $this->createFile($storage_path); } function createFile($file, $parent_id = null){ $name = gettype($file) === 'object' ? $file->getClientOriginalName() : $file; $fileMetadata = new Google_Service_Drive_DriveFile([ 'name' => $name, 'parent' => $parent_id ? $parent_id : 'root' ]); $content = gettype($file) === 'object' ? File::get($file) : Storage::get($file); $mimeType = gettype($file) === 'object' ? File::mimeType($file) : Storage::mimeType($file); $file = $this->drive->files->create($fileMetadata, [ 'data' => $content, 'mimeType' => $mimeType, 'uploadType' => 'multipart', 'fields' => 'id' ]); dd($file->id); } function deleteFileOrFolder($id){ try { $this->drive->files->delete($id); } catch (Exception $e) { return false; } } function createFolder($folder_name){ $folder_meta = new Google_Service_Drive_DriveFile(array( 'name' => $folder_name, 'mimeType' => 'application/vnd.google-apps.folder')); $folder = $this->drive->files->create($folder_meta, array( 'fields' => 'id')); return $folder->id; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 class DriveController extends Controller { private $drive ; public function __construct ( Google_Client $client ) { $this -> middleware ( function ( $request , $next ) use ( $client ) { $client -> refreshToken ( Auth:: user ( ) -> refresh_token ) ; $this -> drive = new Google_Service_Drive ( $client ) ; return $next ( $request ) ; } ) ; } public function getDrive ( ) { $this -> ListFolders ( 'root' ) ; } public function ListFolders ( $id ) { $query = "mimeType='application/vnd.google-apps.folder' and '" . $id . "' in parents and trashed=false" ; $optParams = [ 'fields' = > 'files(id, name)' , 'q' = > $query ] ; $results = $this -> drive -> files -> listFiles ( $optParams ) ; if ( count ( $results -> getFiles ( ) ) == 0 ) { print "No files found.

" ; } else { print "Files:

" ; foreach ( $results -> getFiles ( ) as $file ) { dump ( $file -> getName ( ) , $file -> getID ( ) ) ; } } } function uploadFile ( Request $request ) { if ( $request -> isMethod ( 'GET' ) ) { return view ( 'upload' ) ; } else { $this -> createFile ( $request -> file ( 'file' ) ) ; } } function createStorageFile ( $storage_path ) { $this -> createFile ( $storage_path ) ; } function createFile ( $file , $parent_id = null ) { $name = gettype ( $file ) === 'object' ? $file -> getClientOriginalName ( ) : $file ; $fileMetadata = new Google_Service_Drive_DriveFile ( [ 'name' = > $name , 'parent' = > $parent_id ? $parent_id : 'root' ] ) ; $content = gettype ( $file ) === 'object' ? File:: get ( $file ) : Storage:: get ( $file ) ; $mimeType = gettype ( $file ) === 'object' ? File:: mimeType ( $file ) : Storage:: mimeType ( $file ) ; $file = $this -> drive -> files -> create ( $fileMetadata , [ 'data' = > $content , 'mimeType' = > $mimeType , 'uploadType' = > 'multipart' , 'fields' = > 'id' ] ) ; dd ( $file -> id ) ; } function deleteFileOrFolder ( $id ) { try { $this -> drive -> files -> delete ( $id ) ; } catch ( Exception $e ) { return false ; } } function createFolder ( $folder_name ) { $folder_meta = new Google_Service_Drive_DriveFile ( array ( 'name' = > $folder_name , 'mimeType' = > 'application/vnd.google-apps.folder' ) ) ; $folder = $this -> drive -> files -> create ( $folder_meta , array ( 'fields' = > 'id' ) ) ; return $folder -> id ; } }

In our controller’s constructor, we can’t access Auth user, this is why we’re using a closure based middleware. We’re setting refresh token and creating a new instance of Google_Service_Drive with Google_Client instance from the service container.

Retrieving Folders

In getDrive() method, we’re calling ListFolders($id) method with ‘root’ param. root refers to the main directory of google drive. Then we’re retrieving root folders with files->listFiles($optParams) method. If you want to access files in a specific folder you can pass folder ID instead of root to access its files. You can also recursively calling ListFolder() method in the foreach loop.

if (count($results->getFiles()) == 0) { print "No files found.

"; } else { print "Files:

"; foreach ($results->getFiles() as $file) { dump($file->getName(), $file->getID()) $this->ListFolder($file->getID()) // recursively get all folders and sub folders } } 1 2 3 4 5 6 7 8 9 if ( count ( $results -> getFiles ( ) ) == 0 ) { print "No files found.

" ; } else { print "Files:

" ; foreach ( $results -> getFiles ( ) as $file ) { dump ( $file -> getName ( ) , $file -> getID ( ) ) $this -> ListFolder ( $file -> getID ( ) ) // recursively get all folders and sub folders } }

Creating Files from Storage and Form Uploads

createFile($file, $parent_id = null) takes two arguments. First one is a file. It can be a file object retrieved from requests or a storage path. You can pass the parent id as the second param. If you want to create a new file under a specific folder. After creating the file, it returns file id.

Creating Folders

You can pass the folder name to createFolder($folder_name) method to create a new folder and get its ID.

Deleting Folders

To delete a folder or file, all you have to do is to pass an id to deleteFileOrFolder($id) and it will be deleted from the drive.

I’ve set up an example project repository. If you’ve any issue integrating Google Drive API in your app, comment your issue and I’ll try to help you with it.

Update: August 23rd, 2019

I’ve noticed one similar issue reported in a lot of comments on this post.

{ "error":{ "errors":[ { "domain":"usageLimits", "reason":"dailyLimitExceededUnreg", "message":"Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup.", "extendedHelp":"https://code.google.com/apis/console" } ], "code":403, "message":"Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup." } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "error" : { "errors" : [ { "domain" : "usageLimits" , "reason" : "dailyLimitExceededUnreg" , "message" : "Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup." , "extendedHelp" : "https://code.google.com/apis/console" } ] , "code" : 403 , "message" : "Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup." } }

I tried to debug this error and found that issued token expires after an hour. If you try to make API request after an hour, you get this error as JSON response. Here’s how I fixed this issue. A commit for this fix is also available in the repository.

Create a new migration to add two new columns to the users table. Run

php artisan make:migration add_token_expire_column_to_users --table=users

And add this code up() and down() methods in your migration class.

class AddTokenToUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('users', function (Blueprint $table) { $table->string('token')->after('refresh_token')->nullable(); $table->integer('expires_in')->after('token')->nullable(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('users', function (Blueprint $table) { $table->dropColumn('token'); $table->dropColumn('expires_in'); }); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class AddTokenToUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up ( ) { Schema:: table ( 'users' , function ( Blueprint $table ) { $table -> string ( 'token' ) -> after ( 'refresh_token' ) -> nullable ( ) ; $table -> integer ( 'expires_in' ) -> after ( 'token' ) -> nullable ( ) ; } ) ; } /** * Reverse the migrations. * * @return void */ public function down ( ) { Schema:: table ( 'users' , function ( Blueprint $table ) { $table -> dropColumn ( 'token' ) ; $table -> dropColumn ( 'expires_in' ) ; } ) ; } }

Also, update redirectToGoogleProvider() and handleProviderGoogleCallback() method code in LoginController.

public function redirectToGoogleProvider() { $parameters = [ 'access_type' => 'offline', 'approval_prompt' => 'force' ]; return Socialite::driver('google')->scopes(["https://www.googleapis.com/auth/drive"])->with($parameters)->redirect(); } /** * Obtain the user information from Facebook. * * @return void */ public function handleProviderGoogleCallback() { $auth_user = Socialite::driver('Google')->user(); $data = [ 'token' => $auth_user->token, 'expires_in' => $auth_user->expiresIn, 'name' => $auth_user->name ]; if($auth_user->refreshToken){ $data['refresh_token'] = $auth_user->refreshToken; } $user = User::updateOrCreate( [ 'email' => $auth_user->email ], $data ); Auth::login($user, true); return redirect()->to('/'); // Redirect to a secure page } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public function redirectToGoogleProvider ( ) { $parameters = [ 'access_type' = > 'offline' , 'approval_prompt' = > 'force' ] ; return Socialite:: driver ( 'google' ) -> scopes ( [ "https://www.googleapis.com/auth/drive" ] ) -> with ( $parameters ) -> redirect ( ) ; } /** * Obtain the user information from Facebook. * * @return void */ public function handleProviderGoogleCallback ( ) { $auth_user = Socialite:: driver ( 'Google' ) -> user ( ) ; $data = [ 'token' = > $auth_user -> token , 'expires_in' = > $auth_user -> expiresIn , 'name' = > $auth_user -> name ] ; if ( $auth_user -> refreshToken ) { $data [ 'refresh_token' ] = $auth_user -> refreshToken ; } $user = User:: updateOrCreate ( [ 'email' = > $auth_user -> email ] , $data ) ; Auth:: login ( $user , true ) ; return redirect ( ) -> to ( '/' ) ; // Redirect to a secure page }

Update $fillable array on your User model.

protected $fillable = [ 'name', 'email', 'password', 'refresh_token', 'token', 'expires_in' ]; 1 2 3 protected $fillable = [ 'name' , 'email' , 'password' , 'refresh_token' , 'token' , 'expires_in' ] ;

Now update DriverController constructor.

public function __construct(Google_Client $client) { $this->middleware(function ($request, $next) use ($client) { $accessToken = [ 'access_token' => auth()->user()->token, 'created' => auth()->user()->created_at->timestamp, 'expires_in' => auth()->user()->expires_in, 'refresh_token' => auth()->user()->refresh_token ]; $client->setAccessToken($accessToken); if ($client->isAccessTokenExpired()) { if ($client->getRefreshToken()) { $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken()); } auth()->user()->update([ 'token' => $client->getAccessToken()['access_token'], 'expires_in' => $client->getAccessToken()['expires_in'], 'created_at' => $client->getAccessToken()['created'], ]); } $client->refreshToken(auth()->user()->refresh_token); $this->drive = new Google_Service_Drive($client); return $next($request); }); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public function __construct ( Google_Client $client ) { $this -> middleware ( function ( $request , $next ) use ( $client ) { $accessToken = [ 'access_token' = > auth ( ) -> user ( ) -> token , 'created' = > auth ( ) -> user ( ) -> created_at -> timestamp , 'expires_in' = > auth ( ) -> user ( ) -> expires_in , 'refresh_token' = > auth ( ) -> user ( ) -> refresh_token ] ; $client -> setAccessToken ( $accessToken ) ; if ( $client -> isAccessTokenExpired ( ) ) { if ( $client -> getRefreshToken ( ) ) { $client -> fetchAccessTokenWithRefreshToken ( $client -> getRefreshToken ( ) ) ; } auth ( ) -> user ( ) -> update ( [ 'token' = > $client -> getAccessToken ( ) [ 'access_token' ] , 'expires_in' = > $client -> getAccessToken ( ) [ 'expires_in' ] , 'created_at' = > $client -> getAccessToken ( ) [ 'created' ] , ] ) ; } $client -> refreshToken ( auth ( ) -> user ( ) -> refresh_token ) ; $this -> drive = new Google_Service_Drive ( $client ) ; return $next ( $request ) ; } ) ; }

Now here’s an explanation of what we did here, why it needed to be done and why didn’t I do it when I first published this tutorial. I noticed a few comments coming on this post that has this issue, which I initially ignored and though it was a user error. A few months ago, I was working on a client’s project where we were using Gmail’s API to send emails and we followed the same OAuth flow mentioned in this tutorial to retrieve the token and consume it for sending emails. I noticed the same error that users have been reporting here and figured out I was making a mistake. I learned that Google tokens have an expiry time of one hour and we need to refresh it using refresh_token afterward.

We added two new columns to store expiry time and token issued, which was previously stored in refresh_token column on the user’s record. On redirect back from the OAuth consent screen, we are now updating user’s token, expiry_time, name, and refresh_token if present. I learned that Google only issues refresh token one time after the user accepts the permissions. I’ve set approval_prompt parameter to value force which requests API to display consent screen every time the user tries to log in. You can remove it if you want. And lastly, we updated code in the constructor to check if the user’s access token is expired and request a new using refresh token.