Search code examples
node.jsreactjsgoogle-chrome-extensionchrome-extension-manifest-v3

TypeError: Failed to fetch in google chrome extension app using react


I am trying to create a chrome extension using react and allows users to login with google to my backend server that works with my webapplication front end server. I am able to prompt the user to login using google and retrieve the code using oauth2 flow. However, when I try to fetch my backend route, I am getting the following error:

TypeError: Failed to fetch at background.js:20:7

Here is my chrome extension code: manifest.json

{
  "name": "my chrome extension",
  "description": "my chrome extension description",
  "version": "1.0.0",
  "manifest_version": 3,
  "author": "team creators",
  "icons": {
    "16" : "favicon.ico",
    "48" : "favicon.ico",
    "128" : "favicon.ico"
  },
  "action": {
    "default_popup": "index.html",
    "default_title": "my chrome extension"
  },
  "background": {
    "service_worker": "background.js"
  },
  "oauth2": {
    "client_id": "my client id",
    "scopes": [
      "openid", 
      "profile",
      "email",
      "https://www.googleapis.com/auth/calendar",
      "https://www.googleapis.com/auth/drive",
      "https://www.googleapis.com/auth/documents",
      "https://www.googleapis.com/auth/contacts",
      "https://www.googleapis.com/auth/contacts.readonly",
      "https://www.googleapis.com/auth/contacts.other.readonly",
      "https://mail.google.com/"
    ]
  },
  "content_security_policy": {
    "script-src": ["'self'", "https://accounts.google.com", "'wasm-unsafe-eval'", "'inline-speculation-rules'", "http://localhost:*", "http://127.0.0.1:*"]
  },
  "permissions": [
    "identity",
    "nativeMessaging",
    "tabs"
  ],
  "host_permissions": [
    "https://localhost:*/*",
    "http://localhost:*/*"
  ]
}

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
  
    <title>React App</title>
    <script type="text/javascript" src="oauth2.js"></script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
 
  </body>
</html>

oauth2.js

let clientId = 'my client id'
let redirectUri = `https://${chrome.runtime.id}.chromiumapp.org/`
let nonce = Math.random().toString(36).substring(2, 15)

function addClickEventToButton() {
    const button = document.getElementById('google-login-button');
    if (button) {
        console.log("we get here 3");
        button.addEventListener('click', function() {
            const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
          
            authUrl.searchParams.set('client_id', clientId);
            authUrl.searchParams.set('response_type', 'code');
            authUrl.searchParams.set('redirect_uri', redirectUri);

            var manifest = chrome.runtime.getManifest();
            authUrl.searchParams.set('scope', manifest.oauth2.scopes.join(' '));
            
            // authUrl.searchParams.set('scope', 'openid profile email');
            authUrl.searchParams.set('nonce', nonce);
            // Show the consent screen after login.
            authUrl.searchParams.set('prompt', 'consent');
        
            chrome.identity.launchWebAuthFlow(
                {
                  url: authUrl.href,
                  interactive: true,
                },
                async (redirectUrl) => {
                  if (redirectUrl) {
                    const codeRegex = /code=([^&]+)/;
                    const codeMatch = redirectUrl.match(codeRegex);
                    const code = codeMatch ? codeMatch[1] : null;

                    chrome.runtime.sendMessage({contentScriptQuery: "login", code: code}, function(response) {
                        console.log("reponse", response);
                    });
                    
                  }
                },
            );
        });
    } else {
        setTimeout(addClickEventToButton, 500);
    }
}

document.addEventListener('DOMContentLoaded', function() {
    addClickEventToButton();
});


background.js

chrome.action.onClicked.addListener(function() {
  chrome.tabs.create({url: 'index.html'});
  console.log('action clicked');
});

chrome.runtime.onMessage.addListener(
  async function(request, sender, sendResponse) {
    if (request.contentScriptQuery == "login") {
      console.log("getting request", request);
      const url = "https://localhost:4000/google/auth/login";

      fetch(url, { // errors here
          method: "POST",
          mode: "cors",
          headers: {
              "Content-Type": "application/json",
          },
          body: JSON.stringify({ code: request.code, isExtensionLogin: true }),
      }).then(response => response.json())
        .then(response => sendResponse(response))
        .catch((err) => console.log("err", err))
      
      return true; 
    }
});

Here is my backend server.js with relevant information

app.use((req, res, next) => {
    res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000,https://my-extension-id.chromiumapp.org/,chrome-extension://my-extension-id/"); // update to match the extension domain you will make the request from
    res.setHeader("Access-Control-Allow-Credentials", "true");
    res.setHeader("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT,PATCH,DELETE");
    res.setHeader(
        "Access-Control-Allow-Headers",
        "Access-Control-Allow-Headers, Origin, Accept, X-Requested-With, Content-Type, Authorization, Access-Control-Request-Method, Access-Control-Request-Headers"
    );
    next();
});

route: "https://localhost:4000/google/auth/login"

const handleGoogleSignin = async (req, res) => {
    try {
        const isExtensionLogin = req.body.isExtensionLogin;

        const { tokens } = await oauth2Client.getToken(req.body.code);
        const { sub } = jwt.decode(tokens.id_token);

        // check if user (google login user) exists
        const googleIntegrated = await ThirdParty.findOne({
            thirdPartyId: sub,
        });

        if (!googleIntegrated)
            return res.status(404).json({ message: "Google Account not in DB" });
        if (!googleIntegrated.isSignup)
            return res.status(404).json({
                message: "Google Account not associated with a login account",
        });

        // update google tokens

        ...

        const user = await User.findById(googleIntegrated.userId);
        const userToken = jwt.sign({ id: user._id }, process.env.JWT_SECRET);

        res.status(200).json({ token: userToken, user });
    } catch (err) {
        res.status(409).json({ message: err.message });
    }
};

I found several questions pertaining this topic, but they haven't helped me out Getting "TypeError: Failed to fetch" when the request hasn't actually failed POST API call returns CORS error without https and ERR_CONNECTION_CLOSED without Manifest v3 fetch data from external API How to make a cross-origin request in a content script (currently blocked by CORB despite the correct CORS headers)?


Solution

  • Answering my own question:

    I should be "http://localhost:4000/google/auth/login" not "https://localhost:4000/google/auth/login"