I am implementing a basic login in react, however when the token is saved in sessionStorage the page does not refresh normally like when I do it with hooks.
I use this component to save and return the token when the login is correct.
//UseToken.js
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = sessionStorage.getItem('token');
const userToken = JSON.parse(tokenString);
return userToken
};
const [token, setToken] = useState(getToken());
const saveToken = userToken => {
sessionStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
};
return {
setToken: saveToken,
token
}
}
Later, in the app component, I ask for the status of the token in order to render either the login view or the application view.
//App.js
const {token, setToken} = useToken();
if(!token) {
return <Login setToken={setToken} />
}
Then the token is saved in sessionStorage, however the login is still rendered.
Help
The answer is that, when you set the token, you set it as a non-existent object instead of just passing the value to it.
const saveToken = userToken => {
sessionStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken); // instead of userToken.token
};
Related
tl;dr:
How does one go about setting up a hook to handle token headers across axios api calls in a meaningfully maintainable way, with the assumption that the token itself is exposed as a hook.
I am currently handling authentication by exposing an access token/permissions in a context, and providing a protected route implementation that conditionally exposes the outlet OR a navigation call based on whether the token exists (which is retrieved from the hook).
Initially this works alright, and every component/hook in my application will have access to the hook to get the token. However, what I really want to do now is gain access to that hook where I make my api calls to set up an axios interceptor to manage the auth header for my api calls.
The issue I'm running into is I think any api call will have to be nested within a hook in order for me to use the token on it, and I'm not really sure what that looks like.
I'm using react-query, and was hoping I'd be able to use a mutation to set something to be accessed throughout the app, but that suffers the same pitfall of needing a component to be able to access the hook.
Is it possible to implement a hook for your token - appending middleware with axios?
the protected route implementation:
import React from 'react';
import { Outlet, useLocation, Navigate } from 'react-router-dom';
import { useAuth } from './AuthProvider';
const ProtectedRouterOutlet = () => {
const { token } = useAuth();
const location = useLocation();
if (!token) {
return <Navigate to="/login" replace state={{ from: location }} />;
}
return <Outlet/>;
};
export default ProtectedRouterOutlet;
Auth provider context wrapper
const AuthContext = React.createContext<any>(null);
export const useAuth = () => {
return React.useContext(AuthContext);
};
const loginApiCall = (userName: string, password: string) =>{
if(!userName || !password) { return Promise.reject('Missing Credentials') }
return axios.post(`${auth_service}/oauth/token`, {username: userName, password: password})
}
const AuthProvider = ({ children }: any) => {
const navigate = useNavigate();
const [token, setToken] = React.useState<string | null>(null);
const location = useLocation();
useEffect(() => {
if(location.pathname === '/login' && token) {
navigate('/');
} else if (!token) {
navigate('/login');
}
}, [token])
const loginCall = useMutation( (data: any) => loginApiCall(data.username, data.password), {onSuccess: token => {
console.log('success', token);
setToken(token.data);
// I could do a settimeout here Or use the useeffect hook
// setTimeout(() => navigate('/'))
}})
const handleLogin = async (username: string, password: string) => {
loginCall.mutate({username, password});
};
const handleLogout = () => {
setToken(null);
// todo: call logout api to invalidate token
};
const value = useMemo(() => ({
token,
onLogin: handleLogin,
onLogout: handleLogout,
}), [token]);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;
and the main app file:
const rootComponent = () => {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Shell>
<Outlet/>
</Shell>
</AuthProvider>
</QueryClientProvider>
);
};
EDIT:
I found this (setting defaults in axios), but I'm not sold on it yet:
useEffect(() => {
if(token) { // setting default common header if token exists
axios.defaults.headers.common = {...axios.defaults.headers.common, Authorization: `Bearer ${token.access_token}`};
}
if(location.pathname === '/login' && token) {
navigate('/');
} else if (!token) {
navigate('/login');
}
}, [token])
I am trying to create authentication system with react everything is working. I have one private route if there is no user then it redirects to login page.
This is my private route
import React from 'react'
import { Navigate} from 'react-router-dom'
import { useAuth } from '../../context/AuthContext'
export default function PrivateRoute({children}) {
const { currentUser } = useAuth()
if(!currentUser){
return <Navigate to= '/login' />
}
return children;
}
Problem is after login I get redirect to update-profile page but if I enter login link in address bar it logs out and takes user back to login page. I don't know how to deal with that.
This is my context
import React, {useContext, useEffect, useState} from 'react'
import { auth } from '../firebase-config'
const AuthContext = React.createContext()
export function useAuth(){
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState()
const [loading, setLoading] = useState(true)
function singup(email, password){
return auth.createUserWithEmailAndPassword(email, password)
}
function login(email, password){
return auth.signInWithEmailAndPassword(email, password)
}
function logout(){
return auth.signOut()
}
function resetPassword(email){
return auth.sendPasswordResetEmail(email)
}
function updateEmail(email){
return currentUser.updateEmail(email)
}
function updatePassword(password){
return currentUser.updatePassword(password)
}
useEffect(() =>{
const unsubscribe = auth.onAuthStateChanged(user =>{
setCurrentUser(user)
setLoading(false)
})
return unsubscribe
}, [])
const value = {
currentUser,
login,
singup,
logout,
resetPassword,
updateEmail,
updatePassword
}
return (
<AuthContext.Provider value={value}>
{ !loading && children }
</AuthContext.Provider>
)
}
Issue
Since you are manually entering a URL in the address bar, when you do this it will reload the page, which reloads your app. Anything stored in state is wiped. To keep the state you'll need to persist it to longer-term storage, i.e. localStorage.
Solution
Using localStorage you can initialize the currentUser from localStorage, and use a useEffect hook to persist the currentUser to localStorage.
Example:
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(
JSON.parse(localStorage.getItem("currentUser"))
);
const [loading, setLoading] = useState(true);
useEffect(() => {
localStorage.setItem("currentUser", JSON.stringify(currentUser));
}, [currentUser]);
...
useEffect(() =>{
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
currentUser,
login,
singup,
logout,
resetPassword,
updateEmail,
updatePassword
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
When you re-enter a link in the address bar it deletes all your saved context, if you want a simple way to save the login state, you can save the currentUser object in the localStorage(this is a very basic way, not recommended in real websites), and then when the page loads you can use useEffect to get that data from the localStorage and set currentUser to the user you save in the localStorage.
[I am using next.js for this implementation]
I have this auth provider function:
const AuthContext = createContext({});
export function AuthProvider({ children }) {
const [userCred, setUserCred] = useState()
useEffect(() => {
fetch('/api/userAuth').then(results => results.json()).then(data => setUserCred(data))
}, [userCred]);
return (
<AuthContext.Provider value={{ userCred }}>{children}</AuthContext.Provider>
);
}
export const useAuth = () =>useContext(AuthContext)
I've wrapped that provided into my main _app and it's working fine. The only problem is that getTokenId() always returns empty.
//in main app
import "../styles/globals.css";
import { AuthProvider } from "../context/AuthProvider";
function MyApp({ Component, pageProps }) {
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
);
}
export default MyApp;
However, when I immediately console.log the token from userCredential in my sign in function, the token is not empty.
import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";
const auth = getAuth();
createUserWithEmailAndPassword(auth, email, password) .then((userCredential) => {
// Signed in
const user = userCredential.user;
token = userCredential._tokenResponse.idToken res.status(200).json({userId: user, token: token }); }) .catch((error) => {
const errorCode = error.code;
const errorMessage = error.message; res.status(401).json({errorCode: errorCode })
});
as I see in the firebase user credentials, there are two methods for getting token
getIdTokenResult(
forceRefresh?: boolean
): Promise<firebase.auth.IdTokenResult>;
/**
* Returns a JSON Web Token (JWT) used to identify the user to a Firebase
* service.
*
* Returns the current token if it has not expired. Otherwise, this will
* refresh the token and return a new one.
*
* #param forceRefresh Force refresh regardless of token
* expiration.
*/
getIdToken(forceRefresh?: boolean): Promise<string>;
So you can use these methods, for example.
const token = userCredential.getIdToken();
OR
const token = userCredential.getIdTokenResult();
Also, It always returns a promise, so make sure that you take a result of the promise. Thanks )
I am authenticating my NextJS frontend from a backend that gives me an accessToken on a successful email / password login (Laravel Sanctum). From there I am saving that accessToken in local storage.
If i have a page that needs protecting, for instance /profile, i need to verify that the token is valid before showing the page. If it is not valid, they need to be redirected to the /signin page. So i have the following code which does that.
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export default function Profile() {
const router = useRouter();
useEffect(async () => {
const token = localStorage.getItem('accessToken');
const resp = await fetch('https://theapiuri/api/user', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token
}
});
const json = await resp.json();
if (!token && json.status !== 200) {
router.push('/signin');
}
})
return (
<div>
<h1>Protected Profile Page</h1>
</div>
)
}
It works, sort of. If I am logged out, and i try to visit /profile it will flash up the profile page for a second or so and then redirect to signin.
This doesn't look good at all. I was wondering if anyone in the same situation could share their solution, or if anyone has some advice that would be greatly appreciated.
Your basic problem is that you are returning the profile page immediately, but the token authentication is async. You should wait for the authentication to happen before showing the page. There's different ways to do that, but a basic way is to just set a variable in your state and then change what is returned by the render function based on that variable.
As an example, here I suppose that you have some component that just shows a loader or spinner or something like that:
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import LoaderComponent from 'components/Loader';
export default function Profile() {
const router = useRouter();
const [hasAccess, setHasAccess] = useState(false);
useEffect(async () => {
const token = localStorage.getItem('accessToken');
const resp = await fetch('https://theapiuri/api/user', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token
}
});
const json = await resp.json();
if (!token && json.status !== 200) {
router.push('/signin');
} else {
setHasAccess(true);
}
})
if (!hasAccess) {
return (
<LoaderComponent />
);
}
return (
<div>
<h1>Protected Profile Page</h1>
</div>
)
}
I'm using redux-thunk for async actions, and all was working as expected until I added an Apollo Client into the mix, and I can't figure out why. The action is being dispatched, but the return function is not being called.
-- Client provider so that I can use the client in the Redux store outside of the components wrapped in <ApolloProvider>.
import { ApolloClient, InMemoryCache, createHttpLink } from "#apollo/client";
class ApolloClientProvider {
constructor() {
this.client = new ApolloClient({
link: createHttpLink({ uri: process.env.gqlEndpoint }),
cache: new InMemoryCache(),
});
}
}
export default new ApolloClientProvider();
-- My store setup
const client = ApolloClientProvider.client;
const persistConfig = {
key: "root",
storage: storage,
};
const pReducer = persistReducer(persistConfig, rootReducer);
const store = createStore(
pReducer,
applyMiddleware(thunk.withExtraArgument(client))
);
-- The action
export const fetchMakeCache = (token) => (dispatch, client) => {
console.log("fetching");
const query = gql`
query Source {
sources {
UID
Name
ActiveRevDestination
}
workspaces {
UID
Name
SourceUids
ActiveRevSource
}
}
`;
return () => {
console.log("reached return");
dispatch(requestMakeCache());
client
.query({
query: query,
context: {
headers: {
Authorization: `Bearer ${token}`,
},
},
})
.then((r) => r.json())
.then((data) => dispatch(receivedMakeCache(data)))
.catch((error) => dispatch(failedMakeCache(error)));
};
};
-- The component dispatching the thunk
import React from "react";
import { useAuth0 } from "#auth0/auth0-react";
import { useDispatch } from "react-redux";
import * as actions from "../store/actions";
const Fetch = () => {
const dispatch = useDispatch();
const { getAccessTokenSilently, isAuthenticated } = useAuth0();
if (isAuthenticated) {
// This will set loading = false immediately
// The async function below results in loading not being set to false
// until other components are performing actions that will error
dispatch(actions.requestMakeCache());
(async () => {
const token = await getAccessTokenSilently({
audience: process.env.audience,
});
dispatch(actions.fetchMakeCache(await token));
})();
}
return <></>;
};
export default Fetch;
When the Fetch component loads, the "fetching" console log prints so it's definitely being dispatched. But the "reached return" never gets hit. This exact same code worked as expected when not using the Apollo client. However, I've been able to use the same client successfully in a component. I'm not getting any errors, the return function just isn't being hit.
Most of the questions on here about thunks not running the return function have to do with not dispatching correctly, but I don't think that's the case since this worked pre-Apollo. (Yes, I know that using redux and Apollo together isn't ideal, but this is what I have to do right now)