google tasks API “Error fetching tasks: Error: Failed to fetch tasks: 401 Unauthorized”

I’m trying to build a simple tasks web app where users can login with their google accounts and access google tasks via the API. It’s basically for my own use where I want to add an advanced search functionality. I’m not using and don’t want to use any backend services, simple react, javascript app.

However I’m having errors at the start where the app tries to fetch the tasklists via the google API.

Error response body: {
  "error": {
    "code": 401,
    "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
    "errors": [
      {
        "message": "Invalid Credentials",
        "domain": "global",
        "reason": "authError",
        "location": "Authorization",
        "locationType": "header"
      }
    ],
    "status": "UNAUTHENTICATED"
  }
}
Tasks.js:54
    fetchTasks Tasks.js:54
    Tasks Tasks.js:11
    React 7
    workLoop scheduler.development.js:266
    flushWork scheduler.development.js:239
    performWorkUntilDeadline scheduler.development.js:533
    (Async: EventHandlerNonNull)
    js scheduler.development.js:571
    js scheduler.development.js:633
    factory react refresh:6
    Webpack 24
Error fetching tasks: Error: Failed to fetch tasks: 401 Unauthorized
    fetchTasks Tasks.js:55
    Tasks Tasks.js:11

Also in the network tab, I see 401 Unauthorized error when using GET method to access tasks.googleapis.com

I believe I configured google cloud correctly. I tried to add the screenshots however my message was seen as spam so I removed them.

  • Google Tasks API is enabled.
  • javascript origins dhould be OK.
  • Redirect URLs also should be OK.
  • client ID is correct in .env file.
  • authorized domain are not necessary at this point but:
  • scopes are added
  • test users are added

Here are my files:

const loadGoogleScript = () => {
    return new Promise((resolve) => {
        if (typeof window.google !== 'undefined') {
            resolve();
        } else {
            const script = document.createElement('script');
            script.src = 'https://accounts.google.com/gsi/client';
            script.async = true;
            script.defer = true;
            script.onload = resolve;
            document.body.appendChild(script);
        }
    });
};

export const initializeGoogleAuth = async () => {
    await loadGoogleScript();
    console.log("Using Client ID:", process.env.REACT_APP_GOOGLE_CLIENT_ID);
    window.google.accounts.id.initialize({
        client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
        callback: handleCredentialResponse,
        scope: 'email profile https://www.googleapis.com/auth/tasks'
    });
};

export const handleCredentialResponse = (response) => {
    if (response.error) {
        console.error("Error during Google Sign-In:", response.error);
        return;
    }
    console.log("Encoded JWT ID token: " + response.credential);
    const decodedToken = JSON.parse(atob(response.credential.split('.')[1]));
    console.log("Decoded token:", decodedToken);

    localStorage.setItem('token', response.credential);
    localStorage.setItem('user', JSON.stringify({
        name: decodedToken.name,
        email: decodedToken.email
    }));
    window.location.href = '/welcome';
};

export const renderGoogleSignInButton = () => {
    if (typeof window.google !== 'undefined') {
        window.google.accounts.id.renderButton(
            document.getElementById("googleSignInButton"),
            {
                theme: "outline",
                size: "large",
                type: "standard",
                shape: "rectangular",
                text: "signin_with",
                logo_alignment: "left",
            }
        );
        window.google.accounts.id.prompt();
    } else {
        console.error('Google Sign-In script not loaded');
    }
};

export const isAuthenticated = () => {
    return localStorage.getItem('user') !== null;
};

export const logout = () => {
    localStorage.removeItem('user');
    localStorage.removeItem('token');
    window.location.href = '/';
};

export const getAccessToken = () => {
    return localStorage.getItem('token');
};
import React, { useState, useEffect } from 'react';
import { getAccessToken } from '../services/auth';

const Tasks = () => {
    const [tasks, setTasks] = useState([]);
    const [newTask, setNewTask] = useState('');
    const [searchTerm, setSearchTerm] = useState('');
    const [error, setError] = useState(null);

    useEffect(() => {
        fetchTasks();
    }, []);

    const logRequest = (url, options) => {
        console.log('Outgoing request:', {
            url,
            method: options.method || 'GET',
            headers: options.headers,
            body: options.body
        });
    };

    const logResponse = async (response) => {
        const clone = response.clone();
        const data = await clone.text();
        console.log('Response received:', {
            status: response.status,
            statusText: response.statusText,
            headers: Object.fromEntries(response.headers.entries()),
            data: data
        });
    };

    const fetchTasks = async () => {
        try {
            const accessToken = getAccessToken();
            if (!accessToken) {
                throw new Error('No access token available');
            }

            console.log('Access Token:', accessToken);

            const response = await fetch('https://tasks.googleapis.com/tasks/v1/users/@me/lists', {
                headers: {
                    'Authorization': `Bearer ${accessToken}`
                }
            });

            console.log('Response status:', response.status);
            console.log('Response headers:', JSON.stringify([...response.headers]));

            if (!response.ok) {
                const errorBody = await response.text();
                console.error('Error response body:', errorBody);
                throw new Error(`Failed to fetch tasks: ${response.status} ${response.statusText}`);
            }

            const data = await response.json();
            console.log('Tasks response:', data);
            setTasks(data.items || []);
        } catch (error) {
            console.error('Error fetching tasks:', error);
            console.error('Error stack:', error.stack);
            setError('Failed to fetch tasks. Please try again.');
        }
    };

    const addTask = async () => {
        try {
            const accessToken = getAccessToken();
            if (!accessToken) {
                throw new Error('No access token available');
            }

            const response = await fetch('https://tasks.googleapis.com/tasks/v1/lists/@default/tasks', {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${accessToken}`,
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ title: newTask })
            });

            if (!response.ok) {
                throw new Error('Failed to add task');
            }

            const data = await response.json();
            setTasks([...tasks, data]);
            setNewTask('');
        } catch (error) {
            console.error('Error adding task:', error);
            setError('Failed to add task. Please try again.');
        }
    };

    const deleteTask = async (taskId) => {
        try {
            const accessToken = getAccessToken();
            if (!accessToken) {
                throw new Error('No access token available');
            }

            const response = await fetch(`https://tasks.googleapis.com/tasks/v1/lists/@default/tasks/${taskId}`, {
                method: 'DELETE',
                headers: {
                    'Authorization': `Bearer ${accessToken}`
                }
            });

            if (!response.ok) {
                throw new Error('Failed to delete task');
            }

            setTasks(tasks.filter(task => task.id !== taskId));
        } catch (error) {
            console.error('Error deleting task:', error);
            setError('Failed to delete task. Please try again.');
        }
    };

    const editTask = async (taskId, newTitle) => {
        try {
            const accessToken = getAccessToken();
            if (!accessToken) {
                throw new Error('No access token available');
            }

            const response = await fetch(`https://tasks.googleapis.com/tasks/v1/lists/@default/tasks/${taskId}`, {
                method: 'PATCH',
                headers: {
                    'Authorization': `Bearer ${accessToken}`,
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ title: newTitle })
            });

            if (!response.ok) {
                throw new Error('Failed to edit task');
            }

            const data = await response.json();
            setTasks(tasks.map(task => task.id === taskId ? data : task));
        } catch (error) {
            console.error('Error editing task:', error);
            setError('Failed to edit task. Please try again.');
        }
    };

    const filteredTasks = tasks.filter(task =>
        task.title.toLowerCase().includes(searchTerm.toLowerCase())
    );

    return (
        <div>
            <h2>My Tasks</h2>
            {error && <p style={{ color: 'red' }}>{error}</p>}
            <input
                type="text"
                placeholder="Search tasks"
                value={searchTerm}
                onChange={(e) => setSearchTerm(e.target.value)}
            />
            <div>
                <input
                    type="text"
                    placeholder="Add new task"
                    value={newTask}
                    onChange={(e) => setNewTask(e.target.value)}
                />
                <button onClick={addTask}>Add Task</button>
            </div>
            {filteredTasks.length === 0 ? (
                <p>No tasks found.</p>
            ) : (
                <ul>
                    {filteredTasks.map(task => (
                        <li key={task.id}>
                            {task.title}
                            <button onClick={() => deleteTask(task.id)}>Delete</button>
                            <button onClick={() => editTask(task.id, prompt('Enter new title', task.title))}>Edit</button>
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
};

export default Tasks;

The only successful approach was to use mongoDB at backend but I don’t want to use any backend services.