I created a React application, where I try to set some key in the local storage of the browser. I also set the session storage. When I run my application on development mode, it works – I successfully set keys in both session and local storage. However, on production, only the session storage successfully being set.
This is my code:
import React, { useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { connect } from 'react-redux';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { IAutoAuthResponseData, ICliAuthResponseData } from '@exlint.io/common';
import { authActions } from '@/store/reducers/auth';
import type { IAuthPayload } from '@/store/interfaces/auth';
import BackendService from '@/services/backend';
import CliBackendService from '@/services/cli-backend';
import type { AppState } from '@/store/app';
import ExternalAuthRedirectView from './ExternalAuthRedirect.view';
interface IPropsFromState {
readonly isAuthenticated: boolean | null;
}
interface PropsFromDispatch {
readonly auth: (loginPayload: IAuthPayload) => PayloadAction<IAuthPayload>;
}
interface IProps extends IPropsFromState, PropsFromDispatch {}
const ExternalAuthRedirect: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const refreshToken = searchParams.get('refreshToken');
const port = searchParams.get('port');
useEffect(() => {
if (refreshToken) {
localStorage.setItem('token', refreshToken);
const fetchResults = async () => {
let autoAuthResponseData: IAutoAuthResponseData;
try {
autoAuthResponseData = await BackendService.get<IAutoAuthResponseData>('/user/auth');
console.log(1);
sessionStorage.setItem('token', autoAuthResponseData.accessToken);
} catch {
console.log(2);
localStorage.clear();
let navigateUrl = '/auth';
if (port) {
navigateUrl += `?port=${port}`;
}
navigate(navigateUrl);
return;
}
if (port) {
let cliToken: string;
let email: string;
try {
const responseData = await CliBackendService.get<ICliAuthResponseData>(
'/user/auth/auth',
);
cliToken = responseData.cliToken;
email = responseData.email;
} catch {
navigate(`/cli-auth?port=${port}`);
return;
}
try {
const res = await fetch(`http://localhost:${port}/${cliToken}/${email}`);
if (!res.ok) {
throw new Error();
}
navigate('/cli-authenticated');
} catch {
navigate(`/cli-auth?port=${port}`);
return;
}
}
props.auth({
id: autoAuthResponseData.id,
name: autoAuthResponseData.name,
createdAt: autoAuthResponseData.createdAt,
});
};
fetchResults();
} else {
let navigateUrl = '/auth';
if (port) {
navigateUrl += `?port=${port}`;
}
navigate(navigateUrl);
}
}, [refreshToken, port]);
useEffect(() => {
if (!port && props.isAuthenticated) {
navigate('/', { replace: true });
}
}, [port, props.isAuthenticated]);
return <ExternalAuthRedirectView />;
};
ExternalAuthRedirect.displayName = 'ExternalAuthRedirect';
ExternalAuthRedirect.defaultProps = {};
const mapStateToProps = (state: AppState) => {
return {
isAuthenticated: state.auth.isAuthenticated,
};
};
export default connect(mapStateToProps, { auth: authActions.auth })(React.memo(ExternalAuthRedirect));
As you can see, I have this line of code: localStorage.setItem('token', refreshToken);
.
I can guarantee it is executed, as I see an HTTP request is outgoing off my browser (and the code does call for HTTP request right after this line of code as you can see).
This same code, for some reason, does not set the local storage. However, you can see in the next lines of code, I also set the session storage: sessionStorage.setItem('token', autoAuthResponseData.accessToken);
. Then, the session storage is successfully set also on production.
When I check my console in the browser on production, I see this message: Storage access automatically granted for First-Party isolation “https://api.exlint.io” on “https://app.exlint.io”.
. Not sure how relevant it is.
Note the console.log(1)
and console.log(2)
, I can tell that only console.log(1)
is being logged
I also have middleware of redux to clear storage after logout:
listenerMiddleware.startListening({
actionCreator: authActions.setUnauthenticated,
effect: () => {
listenerMiddleware.stopListening({
actionCreator: authActions.auth,
effect: authEffect,
cancelActive: true,
});
listenerMiddleware.startListening({
actionCreator: authActions.auth,
effect: authEffect,
});
localStorage.clear();
sessionStorage.clear();
},
});
2
Answers
The storage is bound to the source (domain/protocol/port). This means that different protocols or subdomains define different storage objects and they cannot access each other’s data.
I believe that in production mode, the problem is related to changing the subdomain (app.exlint.io / app.exlint.io). After the navigate() call, the call to localStorage.getItem goes to the LocalStorage object associated with another subdomain.
I found a stackoverflow question which has an example solution to your problem. The solution is to use iframe and postMessage to access "shared" storage via the parent domain.
Try to modify your code block to:
This may make sure that your logic executes as expected
Check with some
console.log
statements what happens first:localStorage.clear
orlocalStorage.setItem
. If theclear
happens last, this explains the issue. If that is the case, I suggest you ensure your both local and session storage again in the end of your function, as it seems you have race conditions: