How to logout user in React using context API - reactjs

I am working with React hooks and created login logout functionality, but now I want user to logout when token is expired.
I am on a welcome page and using it, after two minutes if I do something else and the token is expired, I want to logout the user
I have created a context, Auth context where I have login logout context
I just want to call the logout function whenever the token expires
My authContext
const initialstate = {
user: null,
};
if (localStorage.getItem("JWT_Token")) {
const jwt_Token_decoded = Jwt_Decode(localStorage.getItem("JWT_Token"));
console.log(jwt_Token_decoded.exp * 1000);
console.log(Date.now());
if (jwt_Token_decoded.exp * 1000 < Date.now()) {
localStorage.clear(); // this runs only when I refresh the page or reload on route change it dosent work
} else {
initialstate.user = jwt_Token_decoded;
}
}
const AuthContext = createContext({
user: null,
login: (userData) => {},
logout: () => {},
});
const AuthReducer = (state, action) => {
switch (action.type) {
case "LOGIN":
return {
...state,
user: action.payload,
};
case "LOGOUT":
return {
...state,
user: null,
};
default:
return state;
}
};
const AuthProvider = (props) => {
const [state, dispatch] = useReducer(AuthReducer, initialstate);
const login = (userData) => {
localStorage.setItem("JWT_Token", userData.token);
dispatch({
type: "LOGIN",
payload: userData,
});
};
const logout = () => {
localStorage.clear();
dispatch({ action: "LOGOUT" });
};
return (
<AuthContext.Provider
value={{ user: state.user, login, logout }}
{...props}
/>
);
};
export { AuthContext, AuthProvider };
In the above code I am checking for expiry of token, but that only runs when page reloads, here I want to run it in every route change so that I can check for the token expiry.
I don't know how to do that and where to do that.
To logout I just need to call my logout context function, but I don't understand how to make the call.
I don't know If I have to do something in my Axios instance which is a separate file like below. Here I am creating one instance so that I can define my headers and other stuff at one place.
//global axios instance
import axios, { AxiosHeaders } from "axios"; // import axios from axios
const BASE_URL = "https://jsonplaceholder.typicode.com"; // server api
export default axios.create({
baseURL: BASE_URL,
headers: {
"Content-Type": "Application/json",
Access-token: "token here",
},
});
How can I approach this problem? I checked this question but in this example GraphQL has been used, so there is a function to set context where I can pass and use the store to dispatch my logout.
I have shared my axiosInstance code I think something needs to be added there. I am ready to use any approach that will generate some middleware, so that I can check for token in one place.

There are some points to consider in your approach
You can update the log out function to send the user to the login page, this way you guarantee this behavior whenever this user has been logged out;
Use the API response to validate when your token has expired, this way you don't need to keep watching the time of the token expiration and logout the user after any unauthorized API call;
Pay attention to this localStorage.clear() call, sometimes it can clear more than you want, and it is always a good idea to declare what you really want to clear. Eg.: sometimes you want to keep your user language, theme, or some UI configs that shouldn't be cleared after a logout;
Axios has an API to intercept your API call, this is a good way to do something before/after an API call (use it to logout after any unauthorized response)
If you really need to take care of this logout by the client and don't need to await the API response, you can try the setTimeout method (not recommended)

Related

how to execute a component before another one in next.js?

I've been struggling with this problem for a while. I have an Auth component inside which I try to access to local storage to see if there is a token in there and send it to server to validate that token.
if token is valid the user gets logged-in automatically.
./components/Auth.tsx
const Auth: React.FC<Props> = ({ children }) => {
const dispatch = useDispatch(); // I'm using redux-toolkit to mange the app-wide state
useEffect(() => {
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
const userId = localStorage.getItem("userId");
if (userId) {
axios
.post("/api/get-user-data", { userId, token })
.then((res) => {
dispatch(userActions.login(res.data.user)); // the user gets logged-in
})
.catch((error) => {
localStorage.clear();
console.log(error);
});
}
}
}, [dispatch]);
return <Fragment>{children}</Fragment>;
};
export default Auth;
then I wrap every page components with Auth.tsx in _app.tsx file in order to manage the authentication state globally.
./pages/_app.tsx
<Provider store={store}>
<Auth>
<Component {...pageProps} />
</Auth>
</Provider>
I have a user-profile page in which user can see all his/her information.
in this page first of all I check if the user is authenticated to access this page or not.
if not I redirect him to login page
./pages/user-profile.tsx
useEffect(() => {
if (isAuthenticated) {
// some code
} else {
router.push("/sign-in");
}
}, [isAuthenticated]);
The problem is when the user is in user-profile page and reloads . then the user always gets redirected to login-page even if the user is authenticated.
It's because the code in user-profile useEffect gets executed before the code in Auth component.
(user-profile page is a child to Auth component)
How should i run the code in Auth component before the code in user-profile page ?
I wanna get the user redirected only when he's not authenticated and run all the authentication-related codes before any other code.
Are you sure that the problem is that user-profile's useEffect is executed before Auth's useEffect? I would assume that the outermost useEffect is fired first.
What most probably happens in your case is that the code that you run in the Auth useEffect is asynchronous. You send a request to your API with Axios, then the useEffect method continues to run without waiting for the result. Normally, this is a good situation, but in your profile, you assume that you already have the result of this call.
You would probably have to implement an async function and await the result of both the axios.post method and dispatch method. You would need something like this:
useEffect(() => {
async () => {
if (typeof window !== 'undefined') {
const token = localStorage.getItem("token")
const userId = localStorage.getItem("userId")
if (userId) {
try {
const resp = await axios.post("/api/get-user-data", {userId, token})
await dispatch(userActions.login(res.data.user)) // the user gets logged-in
} catch(error) {
localStorage.clear()
console.log(error)
}
}
}
}()
}, [dispatch])
I think this should work, but it would cause your components to wait for the response before anything is rendered.

(jwt refresh token) Persisting a globel state in reactjs

I am currently using the django backend with jwt refresh token to persist a user login on my webpage. I have defined a refresh token hook here to get refresh token when the access token is expired or page is refreshed.
import Axios from '../utils/Axios';
import useAuth from './useAuth';
const useRefreshToken = () => {
const { setAuth } = useAuth();
const refresh = async () => {
const response = await Axios.post('account/auth/refresh/', {
'refresh': localStorage.getItem('refresh_token'),
withCredentials: true
});
setAuth(prev => {
return { ...prev, accessToken: response.data.access }
});
return response.data.access;
}
return refresh;
}
export default useRefreshToken;
After the user refreshed the page, it will trigger the refresh function to obtain another access token by sending out a refresh token to the api endpoint, and using setAuth to assign the new accessToken. And I realized that after I refreshed the page, the auth state will be emptied, making the spread operator of ...prev meaningless. Is there are any ways to presistent the current auth state after refreshing?
I don't really want to use localstore to do that, because my protected route condition depends on rather a user exist, so if I can just use localstore in here, I can just assign a user: 'whatever I type', it will still pass the auth?.user checking.

Protected Route by checking JWT saved in user's cookie

I just finished implementing Google social authentication in my NextJS + DjangoRest project following this blog post. I am trying to figure out how to make protected routes that will redirect users if they’re not logged in.
This is how I did it so far:
when user logs in, it saves the jwt_token in the cookie as httponly
uses axios with “withCredentials: true” to access the API endpoint which returns current user data(i.e. email)
saves the user data as a useContext(). When protected page loads, check if UserContext is empty or not and redirects to login page if it is empty.
The obvious problem is the UserContext is reset whenever user refreshes the page, even when the JWT token is still present in the cookies. And I have a feeling this isn’t the right way to implement this.
So how would I implement a similar feature in a non-hacky way? I cannot read jwt-token from cookies in the frontend as it is httponly. Is there a safe way to read user’s JWT token from cookies to test for authentication?
So if I am reading your question right then you can use getServerSide props on your page to detect if the user is authenticated with your api.
function Page({ isAuth }) {
return (
<>
<div>My secure page</div>
//if you return data from your token check api then you could do something like this
<div>Welcome back {isAuth.name}</div>
</>
)
}
export default Page
export async function getServerSideProps(context) {
const isAuth = await tokenChecker(context.cookies.jwt) // In your token checker function you can just return data or false.
if (!isAuth) { //if tokenChecker returns false then redirect the user to where you want them to go
return {
redirect: {
destination: `/login`,
}
};
}
//else return the page
return {
props: {
isAuth,
},
}
}
If this is not what you mean let me know and i can edit my answer.
I modified #Matt's answer slightly and typescript-friendly to solve my problem. It simply checks the user's cookies if they have a jwt_token value inside.
import cookies from 'cookies'
export const getServerSideProps = async ({
req,
}: {
req: { headers: { cookie: any } };
}) => {
function parseCookies(req: { headers: { cookie: any } }) {
var parsedCookie = cookie.parse(
req ? req.headers.cookie || '' : document.cookie
);
return parsedCookie.jwt_token;
}
const isAuth = parseCookies(req);
if (typeof isAuth === undefined) {
return {
redirect: {
destination: `/auth/sign_in`,
},
};
}
return {
props: {
isAuth,
},
};
};

Expo, Redux Toolkit, set timeout for user slice data (session)

I'm using Redux Toolkit for an Expo app. In this app I have a user.js which is a redux toolkit slice. This slice has auth information (user data, access token, refresh token). The thing is that the session on the server expires after 15 minutes.
What my client requires is to logout the user from the app after 15 minutes.
My question is: how can I achieve this in a redux toolkit slice? One way could be adding a logged_at timestamp to my state and for every API request, check the elapsed time (comparing logged_at > now) but I want to make it the right way and I feel this approach is not very eloquent or usable.
How can I dispatch an action to logout the user after X time has passed using redux/redux toolkit?
Easy Way: Selectors
Storing a timestamp sounds right to me. I might prefer to store the expiration timestamp expiresAt instead of loggedAt.
You can add some logic in a selector function which removes the need for an explicit "logout" action. We can leave expired users in the state but verify them before accessing. This function only returns the user object if it is still valid, and null if it has already expired.
const selectUser = (state) => Date.now() > state.user.expiresAt ? null : state.user.user;
Scheduled Dispatch
You could schedule a dispatch using setTimeout, but you would have to do it at the highest level of your app rather than in the component where you dispatch the login. I created a little demo where it logs out automatically after 10 seconds. A useEffect hooks detects changes to an isLoggedIn property from state (could be derived through a selector) and schedules the logout when isLoggedIn becomes true.
const App = () => {
const isLoggedIn = useSelector((state) => state.user.isLoggedIn);
const dispatch = useDispatch();
useEffect(
() => {
if (isLoggedIn) {
setTimeout(
() => {
dispatch(logOut());
alert("Logged Out");
},
// log out after 10 seconds
10 * 1000
);
}
},
// respond to changes in isLoggedIn
[dispatch, isLoggedIn]
);
return <LogIn />;
};
Redux-Saga
You can schedule a logout in response to a login using redux-saga. You would want to create a non-blocking fork with a delay for 15 minutes. With saga you can also cancel the task if the user logs out on their own before the time is up.
It would be similar to this example for retrying API calls which uses a 2000 ms delay.
Async Thunk
We can schedule a dispatch by using a createAsyncThunk which has a long delay. The user might be already logged out and a different user could be logged in before this resolves. You could handle this by checking the username against the current one either in the reducer or in the thunk.
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
interface User {
username: string;
}
type UserState = User | null;
const initialState = null as UserState;
const wait = (ms: number) =>
new Promise<void>((resolve) => {
setTimeout(() => resolve(), ms);
});
export const asyncLogIn = createAsyncThunk(
"user/asyncLogIn",
async (user: User) => {
await wait(10 * 1000);
return user;
}
);
const userSlice = createSlice({
name: "user",
initialState,
reducers: {
// manual log out
logOut() {
return null;
}
},
extraReducers: (builder) =>
builder
// log in when thunk is initiated
.addCase(asyncLogIn.pending, (state, action) => {
const user = action.meta.arg;
return user;
})
// log out when thunk finally resolves
.addCase(asyncLogIn.fulfilled, (state, action) => {
// only log out if this user is still the logged in user
if (action.payload.username === state?.username) {
return null;
}
})
});
export const { logOut } = userSlice.actions;
export default userSlice.reducer;
Code Sandbox Demo

How to implement login authentication using react-redux?

After a bit of research, JWT is commonly used for login authentication because of its compact nature and easiness to parse. I have settled on using JWT. However, my question is on how to embed this in my redux paradigm. Assuming we have a sign up form, when a user fills in his or her credentials and clicks a submit button, this will invoke an action to create an action to create a JWT. Now, this action goes to the back-end of my application and the back-end of my application calls the JWT API? So this action is an asynchronous/rpc call? Also, how does routing happen exactly? I have used react-router before, but using a boilerplate. I am building this web app from scratch and so I am a bit confused on where to deal with the routing and where do I pass this token exactly that I obtain from the server the first time? Is the token used every time a user does a request? How does the client know about this token every time it does the request so that it would keep a user authenticated?
When a user submits his credentials (email/password) your backend authenticates that for the first time and only this time does the backend use these credentials. On authentication your backend will create a JWT with some of the user information, usually just the user ID. There are plenty of JWT Libraries and even jwt-decode for javascript to do this. The backend will respond with this JWT where the front-end will save it (ie, localStorage.setItem('authToken', jwt)) for every subsequent request.
The user will send a request with the JWT in the request header under the Authorization key. Something like:
function buildHeaders() {
const token = localStorage.getItem('authToken')
return {
"Accept": "application/json",
"Content-Type": "application/json"
"Authorization": `${token}`
}
}
Your backend will now decode and authenticate the JWT. If it's a valid JWT the request continues, if not it's rejected.
Now with React-Router you can protect authenticated routes with the onEnter function. The function you provide does any necessary checks (check localStorage for JWT and if a current user). Typically I've done this:
const _ensureAuthenticated = (nextState, replace) => {
const { dispatch } = store
const { session } = store.getState()
const { currentUser } = session
const token = localStorage.getItem("phoenixAuthToken")
if (!currentUser && token) { // if no user but token exist, still verify
dispatch(Actions.currentUser())
} else if (!token) { // if no token at all redirect to sign-in
replace({
pathname: "/sign-in",
state: { nextPathname: nextState.location.pathname}
})
}
}
You can use this function in any route like so:
<Route path="/secret-path" onEnter={_ensureAuthenticated} />
Check out jwt.io for more information on JWT's and the react-router auth-flow example for more information on authentication with react-router.
I personally use Redux saga for async API calls, and I'll show You the flow I've been using for JWT authorization:
Dispatch LOG_IN action with username and password
In your saga You dispatch LOGGING_IN_PROGRESS action to show e.x. spinner
Make API call
Retrieved token save e.x. in localstorage
Dispatch LOG_IN_SUCCESS or LOG_IN_FAILED to inform application what response did You get
Now, I always used a separate function to handle all my requests, which looks like this:
import request from 'axios';
import {get} from './persist'; // function to get something from localstorage
export const GET = 'GET';
export const POST = 'POST';
export const PUT = 'PUT';
export const DELETE = 'DELETE';
const service = (requestType, url, data = {}, config = {}) => {
request.defaults.headers.common.Authorization = get('token') ? `Token ${get('token')}` : '';
switch (requestType) {
case GET: {
return request.get(url, data, config);
}
case POST: {
return request.post(url, data, config);
}
case PUT: {
return request.put(url, data, config);
}
case DELETE: {
return request.delete(url, data, config);
}
default: {
throw new TypeError('No valid request type provided');
}
}
};
export default service;
Thanks to this service, I can easily set request data for every API call from my app (can be setting locale also).
The most interesting part of it should be this line:
request.defaults.headers.common.Authorization = get('token') ? `Token ${get('token')}` : '';`
It sets JWT token on every request or leave the field blank.
If the Token is outdated or is invalid, Your backend API should return a response with 401 status code on any API call. Then, in the saga catch block, you can handle this error any way You want.
I recently had to implement registration and login with React & Redux as well.
Below are a few of the main snippets that implement the login functionality and setting of the http auth header.
This is my login async action creator function:
function login(username, password) {
return dispatch => {
dispatch(request({ username }));
userService.login(username, password)
.then(
user => {
dispatch(success(user));
history.push('/');
},
error => {
dispatch(failure(error));
dispatch(alertActions.error(error));
}
);
};
function request(user) { return { type: userConstants.LOGIN_REQUEST, user } }
function success(user) { return { type: userConstants.LOGIN_SUCCESS, user } }
function failure(error) { return { type: userConstants.LOGIN_FAILURE, error } }
}
This is the login function of the user service that handles the api call:
function login(username, password) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
};
return fetch('/users/authenticate', requestOptions)
.then(response => {
if (!response.ok) {
return Promise.reject(response.statusText);
}
return response.json();
})
.then(user => {
// login successful if there's a jwt token in the response
if (user && user.token) {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify(user));
}
return user;
});
}
And this is a helper function used to set the Authorization header for http requests:
export function authHeader() {
// return authorization header with jwt token
let user = JSON.parse(localStorage.getItem('user'));
if (user && user.token) {
return { 'Authorization': 'Bearer ' + user.token };
} else {
return {};
}
}
For the full example and working demo you can go to this blog post

Resources