refresh firebase id token server-side - reactjs

I am working on an app with Next.js 13 and firebase auth with id tokens.
I want to leverage Next.JS built-in capability for server-side components to fetch user data faster, therefore I need to verify id tokens on the server at initial request. When no user is logged in on protected routes, I want to redirect to login page.
The problem arises when the user was inactive for >1h and the id token has expired. The next request header will send the expired token causing auth.verifyIdToken to reject it. This will redirect the user to login page, before any client-side code had a chance to run, including user.getIdToken to refresh the token.
Is there a way to refresh the id token on server-side? I read here, that there is a work-around using firebase REST API, which seems insecure.
Context
I use the `firebaseui` [package][2] for login, which creates the initial id token & refresh token. Then I have an `AuthContextProvider` to provide & refresh the id token on the client:
const ServerAuthContextProvider = ({
children,
user,
cookie,
}: {
children: ReactNode;
user: UserRecord;
cookie: Cookie;
}) => {
useEffect(() => {
if (typeof window !== "undefined") {
(window as any).cookie = cookie;
}
return auth.onIdTokenChanged(async (snap) => {
if (!snap) {
cookie.remove("__session");
cookie.set("__session", "", { path: "/" });
return;
}
const token = await snap.getIdToken();
cookie.remove("__session");
cookie.set("__session", token, { path: "/" });
});
}, [cookie]);
return (
<serverAuthContext.Provider
value={{
user,
auth,
}}
>
{children}
</serverAuthContext.Provider>
);
};
);
};
server-side root component
const RootLayout = async ({ children }: { children: React.ReactNode }) => {
const { user } = await verifyAuthToken();
if (!user) redirect("/login");
return (
<html lang="en">
<body>
<ServerAuthContextProvider user={user}>
{children}
</ServerAuthContextProvider>
</body>
</html>
);
};
server-side token verification
const verifyAuthToken = async () => {
const auth = getAuth(firebaseAdmin);
try {
const session = cookies().get("__session");
if (session?.value) {
console.log("found token");
const token = await auth.verifyIdToken(session.value);
const { uid } = token;
console.log("uid found: ", uid);
const user = await auth.getUser(uid);
return {
auth,
user,
};
}
} catch (error: unknown) {
if (typeof error === "string") {
console.log("error", error);
return {
auth,
error,
};
} else if (error instanceof Error) {
console.log("error", error.message);
return {
auth,
error: error.message,
};
}
}
return {
auth,
};
};

Related

Why is my Azure redirect running twice and not stopping the fuction

I want to add the functionality for admins to disable end users access if necessary. It works just fine with non-SSO users. The check will prevent the user from logging in and show them a 'user is not active error'. When a non-active user tries to use Azure SSO to log in, the Azure SSO is still successful and displaying a spinner because there is not an active user. It should not allow them to 'log in' and redirect them to the home page with a displayed error that says 'user is not active'
Here is the function to change the user's isActive status on the backend
const changeUserStatus = asyncHandler(async (req, res) => {
const currentUser = await User.findById(req.user._id);
if (!currentUser) {
res.status(401);
throw new Error('User not found');
}
const user = await User.findByIdAndUpdate(req.params.id, req.body, {
new: true,
});
console.log(user);
res.status(201).json(user);
});
From the backend as well, here is the check for a user's isActive status in the normal login function
//check isActive status
if (user.isActive === false) {
res.status(400);
throw new Error('Not an active user');
}
Here is the check in the Azure SSO log in
if (!user.isActive) {
errors.azure = 'User is no longer permitted to access this application';
res.status(400);
throw new Error(errors.azure);
// console.log(errors);
// return res.status(401).json(errors);
}
Here is my authService.js
// Login user
const login = async (userData) => {
const response = await axios.post(API_URL + 'login', userData);
if (response.data) {
localStorage.setItem('user', JSON.stringify(response.data));
}
return response.data;
};
const azureLogin = async () => {
const response = await axios.get(API_URL + 'az-login');
return response.data;
};
Here is my authSlice
// Login user
export const login = createAsyncThunk('auth/login', async (user, thunkAPI) => {
try {
return await authService.login(user);
} catch (error) {
return thunkAPI.rejectWithValue(extractErrorMessage(error));
}
});
// Login user using AAD - this action sends the user to the AAD login page
export const azureLogin = createAsyncThunk(
'users/azureLogin',
async (thunkAPI) => {
try {
return await authService.azureLogin();
} catch (error) {
return thunkAPI.rejectWithValue(extractErrorMessage(error));
}
}
);
// Login user using AAD - this action redirects the user from the AAD login page
// back to the app with a code
export const azureRedirect = createAsyncThunk(
'users/azureRedirect',
async (code, thunkAPI) => {
try {
return await authService.azureRedirect(code);
} catch (error) {
return thunkAPI.rejectWithValue(extractErrorMessage(error));
}
}
);
And here is the AzureRedirect.jsx component. This is the component that receives the flow from the Microsoft/AAD login page. It is the re-entry point of the application, so to speak.
useEffect(() => {
const code = {
code: new URLSearchParams(window.location.search).get('code'),
};
if (user) {
toast.success(`Logged in as ${user.firstName} ${user.lastName}`);
navigate('/');
} else if (code) {
// This CANNOT run more than once
const error = dispatch(azureRedirect(code));
console.log(error);
} else {
console.log('No code found in URL');
}
}, [dispatch, navigate, user]);
if (!user) {
displayedOutput = <Spinner />;
} else {
displayedOutput = (
<div>
An error has been encountered, please contact your administrator.
<br />
<Link to='/login'>Return to Login</Link>
</div>
);
}
return <div className='pt-4'>{displayedOutput}</div>;

Nextjs App doesn't work in production only in dev mode, return 500 error from firebase

I have an app that works really well in dev mode and even in the build mode of Next js, the repo is there: https://github.com/Sandrew94/space-tourism.
i think the problems is where i get the access_token from firebase in getServerSideProps for strange reason in devMode works and in production don't.
I have follow this guide to get that results https://colinhacks.com/essays/nextjs-firebase-authentication
export const getServerSideProps: GetServerSideProps = async (context) => {
try {
const cookies = nookies.get(context);
const { token } = cookies;
const planetInfo = await fetchPlanetsInfo("destinations", token);
return {
props: {
data: planetInfo || [],
},
};
////////////////////////////////////////////////////////////////
} catch (e) {
context.res.writeHead(302, { Location: "/" });
context.res.end();
return {
redirect: {
permanent: false,
destination: "/",
},
props: {} as never,
};
}
};
or in the context
export const AuthContextProvider = ({ children }: Props) => {
const [user, setUser] = React.useState<any>(null);
React.useEffect(() => {
return auth.onIdTokenChanged(async (user) => {
if (!user) {
setUser(null);
nookies.set(undefined, "token", "", { path: "/" });
} else {
const token = await user.getIdToken();
setUser(user);
nookies.set(undefined, "token", token, { path: "/" });
}
});
}, []);
// force refresh the token every 10 minutes
React.useEffect(() => {
const handle = setInterval(async () => {
const user = auth.currentUser;
console.log(user);
if (user) await user.getIdToken(true);
}, 10 * 60 * 1000);
// clean up setInterval
return () => clearInterval(handle);
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
};
it's so annoying this thing, it works good in devMode i don't know what's changes in production mode.
/////////////////////////////
UPDATE 1
I have done some tests, it seems like the cookie isn't set properly and return undefined ( i check with some console.log) also i get a warning maybe this is the problem
The "token" cookie does not have a valid value for the "SameSite" attribute. Soon cookies without the "SameSite" attribute or with an invalid value will be managed as "Lax". This means that the cookie will no longer be sent to third-party contexts. If the application depends on the availability of this cookie in this type of context, add the "SameSite = None" attribute. For more information on the "SameSite" attribute, see https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite

How to get the token from api in react native

I am using react native and I want to get the access token and the id from api which is created in nodejs using JWT authentication and axios.
Any suggestion please
here is my code below:
Services:
const updatePasswordEmailLink = (id, token, password, passwordConfirm) => {
return (
http.post(`/reset-password/${id}/${token}`, JSON.stringify({ ...{ password, passwordConfirm } }))
)
}
ResetScreen:
useEffect(() => {
//how can I get the id and the token
}, [])
const resetPasswordEmail = async () => {
AuthService.updatePasswordEmailLink(id, token, password, passwordConfirm).then(
() => {
navigation.navigate('LoginScreen');
return true;
})
.catch((error) => {
Alert.alert('Error!', error.message);
return false;
})
}

NextJS cookie token not being detected in getServerSideProps

I've been working through a really decent tutorial about setting up NextJS, firebase, and react-context to handle user authentication. Everything has been going smoothly enough until, well ... the code within my getServerSideProps fails to find the cookie 'token', which causes my firebase query to fail, triggering my redirect to the login page.
So, in short I can login/logout users and set a cookie token. However, when I go to pages that SSR check for the token it doesn't find anything and instead triggers my redirect.
SSR + cookie resource i'm using: https://colinhacks.com/essays/nextjs-firebase-authentication
page SSR request
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
try {
const cookies = nookies.get(ctx);
console.log("cookies token", cookies.token); // returns empty string :(
const token = await firebaseAdmin.auth().verifyIdToken(cookies.token);
// * the user is authenticated
const { uid, email } = token;
// ! stuff would be fetched here
} catch (error) {
// either the `token` cookie doesn't exist
// or the token verification failed
// either way: redirect to login page
return {
redirect: {
permanent: false,
destination: "/auth/login",
},
props: {} as never,
};
}
return {
props: { data, params: ctx.params },
};
};
Context + where I set the cookie
export const AuthContext = createContext<{ user: firebase.User | null }>({
user: null,
});
export function AuthProvider({ children }: any) {
const [user, setUser] = useState<firebase.User | null>(null);
useEffect(() => {
if (typeof window !== "undefined") {
(window as any).nookies = nookies;
}
return firebaseAuth.onIdTokenChanged(async (user) => {
console.log(`token changed!`);
if (!user) {
console.log(`no token found...`);
setUser(null);
nookies.destroy(null, "token");
nookies.set(null, "token", "", {});
return;
}
console.log(`updating token...`);
const token = await user.getIdToken();
// console.log("got user token:", token);
// console.log("got user:", user);
setUser(user);
nookies.destroy(null, "token");
nookies.set(null, "token", token, {});
});
}, []);
// force token refresh every 10 minutes
useEffect(() => {
const handle = setInterval(async () => {
const user = firebaseAuth.currentUser;
if (user) await user.getIdToken(true);
}, 10 * 60 * 1000);
// clean up
return () => clearInterval(handle);
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
}
Solved. I posted my answer to this problem here: https://github.com/maticzav/nookies/issues/255

REACT - useContext state sharing sync issue

Objective : -
I want the user to be able to see his/her orders only if they are logged in. So I am using AuthContext for state management of users logged in data + tokens.
Issue : -
When I pass down the token from AuthContext to child components, AuthContext takes some time to validate the token with the backend and meanwhile the child component's logic breaks.
Child Component (using state/token) :
const MyOrders = () => {
const { userData } = useContext(AuthContext);
const history = useHistory();
if (!userData.token) { // This redirects the user back to the home page immediately
history.push("/"); // because the token hasn't been passed yet when
// the component is loaded
};
const getOrders = async () => {
const url = 'http://localhost:5000/api/orders';
try {
const res = await axios.get(url, {
headers: {
'x-auth-token': userData.token
}
});
console.log(res.data);
} catch (err) {
console.log(err.response);
}
};
useEffect(() => {
if (userData.token) getOrders();
}, []);
Work Around (is it safe ????)
const MyOrders = () => {
const token = localStorage.getItem('auth-token'); // Use localStorage token directly instead of
// validating the token first (from AuthContext)??
const history = useHistory();
if (!token) {
history.push("/");
};
const getOrders = async () => {
const url = 'http://localhost:5000/api/orders';
try {
const res = await axios.get(url, {
headers: {
'x-auth-token': token
}
});
console.log(res.data);
} catch (err) {
console.log(err.response);
}
};
useEffect(() => {
if (userData.token) getOrders();
}, []);
Parent Component (AuthContext) : // in case anyone requires
const AuthContextProvider = (props) => {
const [userData, setUserData] = useState({
token: undefined,
user: undefined
});
//Need to check if user is logged in
//every time the App is rendered
useEffect(() => {
const checkLoggedIn = async () => {
const url = "http://localhost:5000/api/users/token";
let token = localStorage.getItem("auth-token");
//when user is not logged in
if(token === null) {
localStorage.setItem("auth-token", "");
token = "";
}
//need to validate the token if it exists
const tokenResponse = await axios.post(url, null, {
headers: { "x-auth-token": token }
});
//if token is valid, collect user data
if(tokenResponse.data){
console.log(tokenResponse);
setUserData({
token,
user: tokenResponse.data,
});
} else {
localStorage.setItem("auth-token", "");
token = "";
};
};
checkLoggedIn();
}, []);
maybe you can try make a early return just after history.push("/") so rest of the logic will not executed
const MyOrders = () => {
const { userData } = useContext(AuthContext);
const history = useHistory();
if (!userData.token) { // This redirects the user back to the home page immediately
history.push("/");
return (null)
};
const getOrders = async () => {
const url = 'http://localhost:5000/api/orders';
try {
const res = await axios.get(url, {
headers: {
'x-auth-token': userData.token
}
});
console.log(res.data);
} catch (err) {
console.log(err.response);
}
};
useEffect(() => {
if (userData.token) getOrders();
}, []);
I offer you to try to retrieve the saved token in your first page of the app.
use "await" on the function. after finish trying to retrieve from localStorage,
you should navigate to the login if the token doesn't exist, otherwise navigate to the main page.
In this case you won't have a undefined token if you have a token,
But what if the token is undefined somehow? You need to send an unauthorize error from the server if the user trying to fetch with an undefined token, then catch the error in the client side, check the message and the type you received, if it does an authorization error, navigate to the home page.
It is a little bit complicated answer but it sure solve your problem and it will make it more Clean code and SOLID.
You can add loading to your user data, if setting the user data fails then set loading false and you can redirect in your order by returning a Redirect component from react-router.
const AuthContext = React.createContext();
const AuthContextProvider = ({ children }) => {
const [userData, setUserData] = React.useState({
token: undefined,
user: undefined,
loading: true,
});
React.useEffect(() => {
setTimeout(
() =>
setUserData((userData) => ({
...userData, //any other data that may be there
token: 123,
user: 'Hello world',
loading: false, //not loading anymore
})),
5000 //wait 5 seconds to set user data
);
}, []);
return (
<AuthContext.Provider value={userData}>
{children}
</AuthContext.Provider>
);
};
const App = () => {
const { loading, ...userData } = React.useContext(
AuthContext
);
if (!loading && !userData.token) {
//snippet does not have redirect
// but if you have react-router you
// can do this
//return <Redirect to="/" />;
}
React.useEffect(() => {}, []);
return loading ? (
'loading...'
) : (
<pre>{JSON.stringify(userData, undefined, 2)}</pre>
);
};
ReactDOM.render(
<AuthContextProvider>
<App />
</AuthContextProvider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Resources