Protected Route by checking JWT saved in user's cookie - reactjs

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,
},
};
};

Related

How to bypass NextAuth when given an external access token

This is not a common use case and even less a good practice, but for the purposes of my project, if an access token is passed as a parameter in the url (e.g.http://localhost:3000?accessToken=myAccessToken), I need to use it in my API calls and "disable" authentication with next auth.
The authentication process is just a fallback in case an accessToken is not passed.
My current implementation is:
storing the accessToken in a cookie in _app.tsx, before the auth
kicks and redirects to the login page :
_app.tsx
...
// Retrieving the callbackURL query params.
const { callbackUrl } = router.query;
// Retrieving the accessToken from the callbackURL.
const params = new URL(callbackUrl as string, 'https://example').searchParams;
const accessToken = params.get('accessToken');
// Storing it in a cookie.
if (storeNumber) {
document.cookie = `storeNumber=${storeNumber}`;
}
...
in my _middleware.ts file, trying to get this cookie, and authorize
the login if the token is present.
_middleware.ts :
export default withAuth({
pages: {
signIn: '/auth/signin',
},
callbacks: {
authorized: ({ req, token }) => {
const accessToken = getCookie('accessToken'); // => null
return !!accessToken;
},
},
});
I'm not even sure I can access the cookie from the _middleware.ts file, or if it's the right way to do this.
Any help would really be appreciated. Thank you guys.
If anyone wants the solution (doubt it), I managed to retrieve the cookie in the middleware like this :
callbacks: {
authorized: ({ req }) => {
const cookie = req.headers.get('cookie');
const accessToken = cookie.split('accessToken=')[1].split(';')[0];
console.log(accessToken);
// Do your logic
return !!accessToken
},
},

How to redirect back to private route after login in Next.js?

I am building a Next.js project where I want to implement private route similar to react-private route. In React this can be done by using react-router, but in Next.js this cannot be done. next/auth has the option to create private route I guess, but I am not using next/auth.
I have created a HOC for checking if the user is logged-in or not, but I'm not able to redirect the user to the private he/she wants to go after successfully logging in. How to achieve this functionality in Next.js? Can anybody help me in this?
This is the HOC, I used the code from a blog about private routing.
import React from 'react';
import Router from 'next/router';
const login = '/login'; // Define your login route address.
/**
* Check user authentication and authorization
* It depends on you and your auth service provider.
* #returns {{auth: null}}
*/
const checkUserAuthentication = () => {
const token = typeof window !== "undefined" && localStorage.getItem('test_token');
if(!token) {
return { auth: null };
} else return {auth:true};
// change null to { isAdmin: true } for test it.
};
export default WrappedComponent => {
const hocComponent = ({ ...props }) => <WrappedComponent {...props} />;
hocComponent.getInitialProps = async (context) => {
const userAuth = await checkUserAuthentication();
// Are you an authorized user or not?
if (!userAuth?.auth) {
// Handle server-side and client-side rendering.
if (context.res) {
context.res?.writeHead(302, {
Location: login,
});
context.res?.end();
} else {
Router.replace(login);
}
} else if (WrappedComponent.getInitialProps) {
const wrappedProps = await WrappedComponent.getInitialProps({...context, auth: userAuth});
return { ...wrappedProps, userAuth };
}
return { userAuth };
};
return hocComponent;
};
This is my private route code:
import withPrivateRoute from "../../components/withPrivateRoute";
// import WrappedComponent from "../../components/WrappedComponent";
const profilePage = () => {
return (
<div>
<h1>This is private route</h1>
</div>
)
}
export default withPrivateRoute(profilePage);
To redirect back to the protected route the user was trying to access, you can pass a query parameter with the current path (protected route path) when redirecting to the login page.
// Authentication HOC
const loginPath = `/login?from=${encodeURIComponent(context.asPath)}`;
if (context.res) {
context.res.writeHead(302, {
Location: loginPath
});
context.res.end();
} else {
Router.replace(loginPath);
}
In the login page, once the login is complete, you can access the query parameter from router.query.from and use that to redirect the user back.
// Login page
const login = () => {
// Your login logic
// If login is successful, redirect back to `router.query.from`
// or fallback to `/homepage` if login page was accessed directly
router.push(router.query.from && decodeURIComponent(router.query.from) ?? '/homepage');
}
Note that encodeURIComponent/decodeURIComponent is used because the asPath property can contain query string parameters. These need to be encoded when passed to the login URL, and then decoded back when the URL is used to redirect back.

Next.js server side props not loading in time

I'm using supabase and trying to load the user session on the server side. If you refresh the page, it catches there is a user but not on first load (e.g. like when coming from a magic link). How can I ensure it does load before he page?
List item
Here is the page:
import router from "next/router";
import { supabase } from "../utils/supabaseClient";
function Home() {
const user = supabase.auth.user()
if (user){
//router.push('/admin') failsafe, not ideal
}
return (
<div className="min-h-screen bg-elkblue dark:bg-dark-pri">
marketing
</div>
);
}
export async function getServerSideProps({ req }) {
const { user } = await supabase.auth.api.getUserByCookie(req);
if (user) {
return {
redirect: {
destination: "/admin",
permanent: false,
},
};
}
return {
props: { }, // will be passed to the page component as props
};
}
export default Home;
You can use the auth-helpers for help with server-side rendering https://github.com/supabase-community/supabase-auth-helpers/blob/main/src/nextjs/README.md#server-side-rendering-ssr---withpageauth
Do note that it however needs to render the client first after OAuth because the server can't access the token from the URL fragment. The client will then read the token from the fragment and forward it to the server to set a cookie which can then be used for SSR.
You can see an example of that in action here: https://github.com/vercel/nextjs-subscription-payments/blob/main/pages/signin.tsx#L58-L62

React Next js app redirect to login is premature

After a lot of searching for several hours, I have the following code to redirect from a user profile page if not logged in.
NOTE: Simply showing a not authorized page is easy but its the redirect thats messing things up.
The code does the job of redirecting when user is not logged in.
const Dashboard = () => {
const [user, { mutate }] = useCurrentUser();
const router = useRouter();
useEffect(() => {
// redirect to login if user is not authenticated
if (!user) router.push('/login');
}, [user]);
...
The problem is when a user is logged in and directly goes to /user/dashboard route, for a split second, user is undefined may be so it redirects to login. When it gets to login, it finds that user is authenticated so redirects to home page because I am redirecting a logged in user to home page.
How to prevent that split second of "not a user" status when page is first loading?
I tried -
getInitialProps
getServerSideProps - Cant use router because next router can only be used on client side
componentDidMount - UseEffectI tried above is the equivalent correct?
Edit: Based on answer below, I tried this but still directly takes user to login first. I am using react cookies and I do see loggedIn cookie as true when user is logged in and its not set when user is not logged in.
Dashboard.getInitialProps = ({ req, res }) => {
console.log(req.headers.cookie)
var get_cookies = function(request) {
var cookies = {};
request.headers && request.headers.cookie.split(';').forEach(function(cookie) {
var parts = cookie.match(/(.*?)=(.*)$/)
cookies[ parts[1].trim() ] = (parts[2] || '').trim();
});
return cookies;
};
//console.log(get_cookies(req)['loggedIn']);
if (get_cookies(req)['loggedIn'] == true) {
console.log("entered logged in")
return {loggedIn: true};
}
else {
console.log("entered not logged in")// can see this on server console log
// User is not logged in, redirect.
if (res) {
// We're on the server.
res.writeHead(301, { Location: '/login' });
res.end();
} else {
// We're on the client.
Router.push('/login');
}
}
}
You can implement redirect when not authenticated in getServerSideProps
Below example is based on JWT Authentication with cookies.
export const getServerSideProps = async (ctx) => {
const cookie = ctx.req.headers.cookie;
const config = {
headers: {
cookie: cookie ?? null
}
}
let res;
try {
// your isAuthenticated check
const res = await axios('url', config);
return { props: { user: res.data } };
} catch (err) {
console.error(err);
ctx.res.writeHead(302, {
Location: 'redirectUrl'
})
ctx.res.end();
return;
return { props: { user: null } };
}
}
You should be able to use getInitialProps to redirect. You just need to check whether you're on the server or the client and use the proper redirect method. You can't use hooks in getInitialProps so your useCurrentUser approach won't work and you'll need some other way to check whether the user is authed. I don't know anything about the structure of your application, but it's probably just some kind of request to wherever you're storing the session.
import Router from 'next/router';
const Dashboard = (props) => {
// props.user is guaranteed to be available here...
};
Dashboard.getInitialProps = async ({ res }) => {
// Check authentication.
// Await the response so that the redirect doesn't happen prematurely.
const user = await ...
// User is logged in, return the data you need for the page.
if (user) {
return { user };
}
// User is not logged in, redirect.
if (res) {
// We're on the server.
// Make the redirect temporary so it doesn't get cached.
res.writeHead(307, { Location: '/login' });
res.end();
} else {
// We're on the client.
Router.push('/login');
}
};
After many hours of struggle, there was one number that was breaking this.
Instead of
res.writeHead(301, { Location: '/login' });
I used
res.writeHead(307, { Location: '/login' });
and it worked.
301 is a permanent redirect so if we use that, when the user logs in, the browser still holds the redirect cache.
From next js docs
Next.js allows you to specify whether the redirect is permanent or not with the permanent field. This is required unless you need to specify the statusCode manually
When permanent is set to true we use a status code of 308 and also set a Refresh header for backwards compatibility with IE11.
When permanent is set to false we use a status code of 307 which is not cached by browsers and signifies the redirect is temporary.
Next.js permits the following status codes:
-301 Moved `Permanently`
-302 Found
-303 See Other
-307 `Temporary` Redirect
-308 Permanent Redirect

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