• Reading time ~ 9 min
  • 15.01.2024

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.

Creating new project - React Laravel API
Creating new project “React Laravel API”

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.

Correct project selected and configure consent button
Correct project selected and configure consent button

For the sake of this example, we will configure User Type as external.

Google OAuth consent screen configuration
OAuth consent screen configuration

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.

Creating new OAuth client ID
Creating new OAuth client ID

Select “Web application” as Application type and give it friendly name.

Application type and Name
Application type and 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”.

JavasScript origin and Authorized redirect URI
JavasScript origin and Authorized redirect URI

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.

Client details
Client details

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.

http://localhost - Fresh Laravel installation
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.

http://localhost:3000 - Fresh React installation
http://localhost:3000

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.

Google Sign In form
Google Sign In 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

  1. Laravel Installation docs – https://laravel.com/docs/9.x/installation#choosing-your-sail-services
  2. Laravel Socialite – https://laravel.com/docs/9.x/socialite
  3. Laravel Sanctum, issuing API tokens – https://laravel.com/docs/9.x/sanctum#issuing-api-tokens
  4. React, installation – https://create-react-app.dev/docs/getting-started
  5. 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]!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

ABOUT

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

About author CrazyBoy49z
WORK EXPERIENCE
Contact
Ukraine, Lutsk
+380979856297