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
- install google's apiclient
- set up a controller
- build a google client
- return an authorization url
- test the authorization url
- build a login endpoint
- test the login
- all about refresh tokens
- google drive access (at last)
- putting it all together
- setting up some google api credentials
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
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',
],
],
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');
});
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'
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
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"
]
}
}
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
{
}
one thing we notice is that we are not use
ing 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
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
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');
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'
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
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.
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
the important thing here is value for code
on the query string. in the above example it is:
4%2F0AX4XfWisBNVvCTCgMOXK-sB69E7WeAK3i6Jt_pcfQeJyMs4l2IOgvjDzNUbAKHsMWa88ng
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();
});
}
}
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
and now we are good to run our migrations!
php artisan migrate:fresh
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',
];
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
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');
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'
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>
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
}
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
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);
}
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');
});
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
}
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');
});
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'.
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.
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'
and then scroll down until you see the big button for 'Google Drive API'. click it.
then all we need to do is click the big blue 'enable' button we are presented with.
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'
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.
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'
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.
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.
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'.
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.
from the 'create credentials' menu at the top, we select 'Oauth Client ID'.
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'
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.
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"
]
}
}
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 (2)
Great article!
Comprehensive and as succinct as possible for what is pretty complex functionality.
Very helpful, Please make that of Facebook as an extension of this