Since I configured my React app to be a PWA, I started having issues with google authentication.
When the user clicks on sign-in with Google, this endpoint is supposed to be hit:
// @route GET api/users/auth/google
// @desc Authenticate user using google
// @access Public
router.get(
"/auth/google",
passport.authenticate("google", {
scope: [
"email",
"profile",
"https://www.googleapis.com/auth/user.birthday.read",
"https://www.googleapis.com/auth/user.gender.read",
"https://www.googleapis.com/auth/user.addresses.read",
],
})
);
But, since the PWA uses caching, the client browser does not even communicate with the server. So that endpoint doesn't get hit.
And what happens is that it loads the cached React app, and uses react routing, and because of it when the user clicks on the Sign in with Google button, and this route gets called:
Instead of communicating with the server, it uses react routing and the user gets redirected to the home page because of this:
<Redirect to="/home" />
I had to disable the PWA in order for the sign-in to work all the time. However, this had the side-effect of disabling caching and other PWA features like installation.
The solution is to prevent the use of caching when this route is called:
This is a similar question but it uses workbox which my app doesn't.
This is my serviceWorker file:
import axios from "axios";
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
window.location.hostname === "[::1]" ||
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
checkValidServiceWorker(swUrl, config);
} else {
console.log(
"Is not localhost. Just register service worker, calling registerValidSW"
);
registerValidSW(swUrl, config);
}
});
}
}
async function subscribeToPushNotifications(serviceWorkerReg) {
console.log("subscribeToPushNotifications is called");
let subscription = await serviceWorkerReg.pushManager.getSubscription();
if (subscription === null) {
const dev_public_vapid_key =
"BLhqVEcH5_0uDpSlwKfvNk7q5IwM5uxYf2w8qvHqdk0SrBpQMGKIZfBrlG-1XYvGxHZXHSik3pQ8IN8NeNCYRtU";
const prod_public_vapid_key =
"BBagXPEL91hwEird3KIG2WuxcWt0hOq1AA7QKtK1MlNqMxiBgQ_RCT8f7rCwYIkuHSVg65Xm68lIlGobXDT1yDI";
const public_vapid_key = isLocalhost
? dev_public_vapid_key
: prod_public_vapid_key;
subscription = await serviceWorkerReg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: public_vapid_key,
});
axios
.post("/api/push_notif_subscription/subscribe", subscription)
.then((response) => {})
.catch((error) => {
console.log(error);
});
}
}
function serviceWorkerRegistrationEnhancements(config, registration) {
registration.addEventListener("activate", function (event) {
event.waitUntil(() => {
if (config && config.onActivated) {
config.onActivated(registration);
}
});
});
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker.register(swUrl).then((registration) => {
console.log("Line right before calling subscribeToPushNotifications");
subscribeToPushNotifications(registration);
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (!installingWorker) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
serviceWorkerRegistrationEnhancements(config, registration);
registration.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
});
}
export function showPushNotification(title, description, image) {
if (!("serviceWorker" in navigator)) {
console.log("Service Worker is not supported in this browser");
return;
}
navigator.serviceWorker.ready.then(function (registration) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
});
}
function checkValidServiceWorker(swUrl, config) {
fetch(swUrl, {
headers: { "Service-Worker": "script" },
}).then((response) => {
const contentType = response.headers.get("content-type");
if (
response.status === 404 ||
(!!contentType && contentType.indexOf("javascript") === -1)
) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
console.log("Service worker found, calling registerValidSW");
registerValidSW(swUrl, config);
}
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}
}
Service workers allow for very granular control of caching behavior based on URL.
By default, your PWA (Progressive Web App) is caching all the assets, including your API calls, due to which you are having issues with Google authentication. You will have to exclude a specific route from being cached, specifically
/api/users/auth/google
.From your
serviceWorker.js
file, I noticed that you are not handlingfetch
events, which are typically used to manage caching and offline support.See for example this "
simple-service-worker/sw.js
".You would need to implement that part first, so you can control what gets cached.
In the
registerValidSW
function, after theonupdatefound
event listener, add afetch
event listener where you can specify the logic for caching:Here, you first check if the method is
GET
. If it is not, skip it.Then, check if the request is for the
/api/users/auth/google
route. If it is, also skip it.For other routes, you perform a fetch from the cache, and if it is not present in the cache, you fetch it from the network, cache it, and then serve it to the client.
(Do replace
CACHE_NAME
with the actual cache name that you intend to use).That code should ideally be inside a service worker file (e.g.,
service-worker.js
) that is separate from the main JavaScript bundle that your web application uses.I see that your
serviceWorker.js
file (which appears to be a part of your main JavaScript bundle, given it imports other modules likeaxios
) tries to register a separate service worker from theservice-worker.js
file. If the service worker file you have provided is indeed part of your main JavaScript bundle, you should ideally move this fetch event handling code to the separateservice-worker.js
file that is being registered as a service worker. That separate service worker file should not use modern ES6 imports and should be written in a way that it can be served as a standalone script file. You would also need to handle caching the appropriate files during theinstall
andactivate
events of the service worker.Again, the
service-worker/simple-service-worker/sw.js
is a good example.Service workers are designed to provide control over the network requests made by a web page, allowing you to intercept those requests and provide custom responses, such as serving cached content when offline.
They do not generally have control over navigation requests made by entering a URL in the browser's address bar, as those requests are made at a higher level than the page itself.
When you are dealing with a request to an external URL that should always hit the server (such as an API endpoint), you can specifically tell the service worker to bypass the cache and make a network request. Here is how you could set up your
fetch
event listener to do that for the URL path you have mentioned:That would set up a
fetch
event listener inside the service worker that checks if the requested URL ends with the specific path you have mentioned. If it does, it responds to the request with a network fetch, bypassing any cached content. Other requests can be handled differently, possibly using cached content as appropriate.Note: Make sure that the URL path is exactly matched with what you are requesting; adjust the condition accordingly.
That will make sure that any requests to that specific URL will always hit the network, rather than using cached content. But it will not have any effect on navigation requests made by typing a URL directly into the address bar, as those are not controlled by the service worker's
fetch
event listener.If you want to make sure that the entire page is always fetched from the network, rather than being served from the cache, you may need to adjust the overall caching strategy used by your service worker, possibly using the
NetworkOnly
strategy provided by some service worker libraries like Workbox.True: Redirecting the entire page using
window.location.href
would indeed cause a full-page navigation request that is not controlled by the service worker.In such scenarios, you might want to employ a different strategy for handling these authentication requests.
For instance, instead of directly changing
window.location.href
, you can use fetch or AJAX to hit the endpoint. That way, you will be making a request that can be intercepted by a service worker. However, this might be tricky with OAuth redirection flows.Or, when setting
window.location.href
, append a unique cache-busting parameter to the URL. That would make each request unique, and thus it will not be served from the cache.Make sure your server sets the appropriate Cache-Control headers for the URL, indicating that the request should not be cached. That header will advise the service worker not to cache this request.
If you are using a library like Workbox to control your caching behavior, you can explicitly tell it to never cache certain URLs. Though this might not work if you are using full-page navigation as in your case.
TomG adds in the comments