This post was inspired by this awesome medium post and can be considered as supplementary post where Google login is implemented in environment with separate React app and Laravel API.
1. Creating Google project
Create new google project here: https://console.developers.google.com/projectcreate.
Once the project is created, proceed by creating new “OAuth 2.0 Client ID”.
First, make sure correct project is selected and then proceed with consent screen configuration by clicking on “CONFIGURE CONSENT SCREEN” button.
For the sake of this example, we will configure User Type as external.
After you fill out the rest of the form (you can just fill in required fields and leave rest as default), let’s finally create OAuth Client.
Select “Web application” as Application type and give it friendly name.
Next, add “Authorized JavasScript origins” and Authorized redirect URIs”. Since we will be creating React app with npx create-react-app
and it runs the app on http://localhost:3000
by default, we will add that as authorized origin. Our redirect URI will be a route /auth/google
so we add absolute url of that route to “Authorized redirect URIs”.
After filling up the details, click on create and you will get credentials in modal. Write them down or download JSON because we will need them in our Laravel app setup.
That’s it. Let’s proceed with creating Laravel API.
2. Creating Laravel API
2.1. Installing and configuring Laravel and required packages
First, let’s create new Laravel app. On MacOS, you can run the following command to install new Laravel app into “laravel” folder with Laravel Sail (mysql).
curl -s "https://laravel.build/laravel?with=mysql" | bash
As suggested at the end of command, run
cd laravel && ./vendor/bin/sail up
If everything went well, your app should be up and running on http://localhost
.
Since Laravel now ships with Laravel Sanctum by default, we will be using it for API requests authentication. However, before we can issue API token for the user, we must “Sign it in” with Google. To do that, we will be using Laravel Socialite, so let’s proceed by adding it.
composer require laravel/socialite
To finish the setup, we need to adjust config/services.php
file and add env variables.
// config/services.php
return [
...
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_REDIRECT_URI'),
],
];
GOOGLE_CLIENT_ID
and GOOGLE_CLIENT_SECRET
you got in the last step of “Creating Google project” part. GOOGLE_REDIRECT_URI
is the same as the one you configured in “Authorized Redirect URI” in Google project. This points to React App!!
// .env
GOOGLE_CLIENT_ID=783523056435-2901krcqpbe6a08q0gls6ifvha8lrd10.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google
2.2. Adjusting migrations and models
Next, let’s adjust the default migrations. Normally we would add new migration, but since we still haven’t migrated the DB, we can just simply change the default one. In this example we will add 2 fields. First one is required field google_id
where users google id will be stored and second one is optional avatar
field. You can check and store any other field that Google returns per your preference.
// database/migrations/2014_10_12_000000_create_users_table.php
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password')->nullable(); // Changed to nullable()
$table->string('google_id'); // Added
$table->string('avatar')->nullable(); // Added
$table->rememberToken();
$table->timestamps();
});
}
And now we are ready to migrate the DB so you can run the following to run the migrations from Sail.
./vendor/bin/sail artisan migrate
Also, don’t forget to reflect the changes on the User model.
// app/Models/User.php
protected $fillable = [
'name',
'email',
'email_verified_at', // Fillable since we will manually add it on Sign on
'password',
'google_id', // Added
'avatar', // Added
];
2.3. Adding the routes and the controller logic.
First, we will need 2 new routes in routes/api.php
.
// routes/api.php
use App\Http\Controllers\AuthController;
Route::get('auth', [AuthController::class, 'redirectToAuth']);
Route::get('auth/callback', [AuthController::class, 'handleAuthCallback']);
redirectToAuth
method is straight forward. It just generates Google redirect url and returns it. Make sure you use stateless()
since we are using this Laravel app as API and we are not keeping state at any time.
// app/Http/Controllers/AuthController.php
<?php
namespace App\Http\Controllers;
use App\Models\User;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Http\JsonResponse;
use Laravel\Socialite\Contracts\User as SocialiteUser;
use Laravel\Socialite\Facades\Socialite;
class AuthController extends Controller
{
public function redirectToAuth(): JsonResponse
{
return response()->json([
'url' => Socialite::driver('google')
->stateless()
->redirect()
->getTargetUrl(),
]);
}
}
handleAuthCallback
contains the logic to handle the callback. To keep it simple, we will just check if user is correctly authenticated, firstOrCreate the User and respond with the User and newly generated bearer token. Tokens are generated with Laravel Sanctum $user->createToken('google-token')->plainTextToken
.
Normal use case would probably either re-use authentication token or invalidate previous ones, but that is up to you to implement and not in the scope of this example.
// app/Http/Controllers/AuthController.php
<?php
namespace App\Http\Controllers;
use App\Models\User;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Http\JsonResponse;
use Laravel\Socialite\Contracts\User as SocialiteUser;
use Laravel\Socialite\Facades\Socialite;
class AuthController extends Controller
{
public function handleAuthCallback(): JsonResponse
{
try {
/** @var SocialiteUser $socialiteUser */
$socialiteUser = Socialite::driver('google')->stateless()->user();
} catch (ClientException $e) {
return response()->json(['error' => 'Invalid credentials provided.'], 422);
}
/** @var User $user */
$user = User::query()
->firstOrCreate(
[
'email' => $socialiteUser->getEmail(),
],
[
'email_verified_at' => now(),
'name' => $socialiteUser->getName(),
'google_id' => $socialiteUser->getId(),
'avatar' => $socialiteUser->getAvatar(),
]
);
return response()->json([
'user' => $user,
'access_token' => $user->createToken('google-token')->plainTextToken,
'token_type' => 'Bearer',
]);
}
}
That’s it. You can find this whole example with tests in this GitHub repository.
3. Creating React App
3.1 Installing React app
Let’s first create new React app with npx
.
npx create-react-app react-app
cd react-app
npm start
If all went well, when you visit http://localhost:3000
, you should see something like this.
3.2. Adding react-router-dom
and configuring routes
For simplicity, let’s also add react-router-dom
.
npm install --save react-router-dom
Now we can setup our routes in App.js
file.
// src/App.js
import './App.css';
import {Route, BrowserRouter, Routes} from "react-router-dom";
import SignIn from "./SignIn";
import GoogleCallback from "./GoogleCallback";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<SignIn />}></Route>
<Route path="/auth/google" element={<GoogleCallback />}></Route>
</Routes>
</BrowserRouter>
);
}
export default App;
3.3. Getting Google redirect url and redirecting to sing in form
SignIn
component is simple. On load, we will fetch Google redirect url from Laravel API and set it as href
for our link.
// src/SignIn.js
import React, {useState, useEffect} from 'react';
function SignIn() {
const [loginUrl, setLoginUrl] = useState(null);
useEffect(() => {
fetch('http://localhost:80/api/auth', {
headers : {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error('Something went wrong!');
})
.then((data) => setLoginUrl( data.url ))
.catch((error) => console.error(error));
}, []);
return (
<div>
{loginUrl != null && (
<a href={loginUrl}>Google Sign In</a>
)}
</div>
);
}
export default SignIn;
When user clicks on “Google Sign In” link, page will redirect to google authentication form.
After successful authentication with Google account, Google will redirect back to the URL we setup in Laravel app .env variable GOOGLE_REDIRECT_URI
with some additional data in search
parameters.
3.4. Processing return callback and authenticating user requests
In our second component , GoogleCallback
, we will take these search
parameters and “proxy” them to Laravel API. If all goes well, Laravel will respond with newly created/fetched User and new Bearer authorization token which we can use to make authenticated calls to Laravel API sanctum protected routes.
// src/GoogleCallback.js
import React, {useState, useEffect} from 'react';
import {useLocation} from "react-router-dom";
function GoogleCallback() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState({});
const [user, setUser] = useState(null);
const location = useLocation();
// On page load, we take "search" parameters
// and proxy them to /api/auth/callback on our Laravel API
useEffect(() => {
fetch(`http://localhost:80/api/auth/callback${location.search}`, {
headers : {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then((response) => {
return response.json();
})
.then((data) => {
setLoading(false);
setData(data);
});
}, []);
// Helper method to fetch User data for authenticated user
// Watch out for "Authorization" header that is added to this call
function fetchUserData() {
fetch(`http://localhost:80/api/user`, {
headers : {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + data.access_token,
}
})
.then((response) => {
return response.json();
})
.then((data) => {
setUser(data);
});
}
if (loading) {
return <DisplayLoading/>
} else {
if (user != null) {
return <DisplayData data={user}/>
} else {
return (
<div>
<DisplayData data={data}/>
<div style={{marginTop:10}}>
<button onClick={fetchUserData}>Fetch User</button>
</div>
</div>
);
}
}
}
function DisplayLoading() {
return <div>Loading....</div>;
}
function DisplayData(data) {
return (
<div>
<samp>{JSON.stringify(data, null, 2)}</samp>
</div>
);
}
export default GoogleCallback;
That’s it! You now have fully functioning Google sign in with React and Laravel API.
You can find the whole project in this GitHub repository.
External resources
- Laravel Installation docs – https://laravel.com/docs/9.x/installation#choosing-your-sail-services
- Laravel Socialite – https://laravel.com/docs/9.x/socialite
- Laravel Sanctum, issuing API tokens – https://laravel.com/docs/9.x/sanctum#issuing-api-tokens
- React, installation – https://create-react-app.dev/docs/getting-started
- React, adding router – https://create-react-app.dev/docs/adding-a-router
Do you need help with setting up Google sign in with React and Laravel API?
For anything related to Laravel development send us your inquiry to [email protected]!
Thankyou bro so helpful🔥🔥
how do we logout from Google login ??
Thank you Samuel!
Since Laravel Sanctum is used, logout should be pretty straight forward. You basically need to:
Let me know if this worked for you or if you need additional assistance.
Hi! Thanks for this. I was completely lost and it was really helpful.
I have a doubt though. I implemented this exactly as you said but I’m getting this error:
GuzzleHttp\Exception\ClientException: Client error: `POST https://oauth2.googleapis.com/token` resulted in a `400 Bad Request` response:
{
“error”: “invalid_grant”,
“error_description”: “Bad Request”
}
I’ve been trying to debug it and searched for hours in google but couldn’t find a solution. Did you, by any chance, encounter this error?
I think google is calling handleAuthCallback two times because i can see briefly the correct returned data in the browser before seeing ‘Invalid credentials provided.’
Thanks again for this post.
Thank you Débora!
This looks like backend issue (because of guzzle). handleAuthCallback must be called from react app, not by Google.
The flow should be the following:
1. React calls Laravel /auth to get redirect url
2. Laravel sends back redirect url and React adds it to the button
3. User clicks on button and is redirected to Google
4. User logs in and is redirected back to React (that is configured in Laravel .env var GOOGLE_REDIRECT_URI)
5. React receives data on /auth/google endpoint (GoogleCallback component)
6. React calls Laravel /auth/callback and sends data it received from google callback along the way (that’s ${location.search} part)
7. Laravel can, from that info React sent along the way, get the user data with Socialite::driver(‘google’)->stateless()->user(); – take note that this is google user, not local user from users table
8. Once Laravel has the user it firstOrCreate the user locally and issues access token ($user->createToken(‘google-token’)->plainTextToken)
9. That token, along with other user data, is returned to React
10. React uses that token to authorize every next request to Laravel (fetchUserData function)
My best guess is that something is not correctly configured in Google. Most probably user type in consent screen is configured as internal and you are trying to log in with external user. I would suggest to try to reconfigure Google from scratch following the steps exactly.
If you are still having troubles, feel free to contact me directly at [email protected].
Hi! Thanks for your reply.
After a lot of time spent, I realized that React was re-rendering because Strict Mode was on. After removing that, the problem was solved. This was the StackOverflow post that gave me the clue: https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar
Thanks for your help!
Hello! I have a problem, when I log in to Google, it redirects to the laravel server – http://127.0.0.1:8000/. Why?
Did you followed the guide correctly? Specifically referring to this point – “GOOGLE_REDIRECT_URI is the same as the one you configured in “Authorized Redirect URI” in Google project. This points to React App!!”
You must configure GOOGLE_REDIRECT_URI correctly to point to frontend app instead of Laravel server.
I have an issue, sometimes it works and sometimes it doesn’t. It usually returns invalid credentials but when I make changes in the codes and save. It works. What could be the reason?