I have a simple project that I built that protects the routes/pages of the website by using the if and else statement and putting each page with a function withAuth(), but I'm not sure if that is the best way to protect routes with nextjs, and I noticed that there is a delay in protecting the route or pages, like 2-3 seconds long, in which they can see the content of the page before it redirects the visitor or unregistered user to the login page.
Is there a way to get rid of it or make the request faster so that unregistered users don't view the page's content? Is there a better approach to safeguard a certain route in the nextjs framework?
Code
import { useContext, useEffect } from "react";
import { AuthContext } from "#context/auth";
import Router from "next/router";
const withAuth = (Component) => {
const Auth = (props) => {
const { user } = useContext(AuthContext);
useEffect(() => {
if (!user) Router.push("/login");
});
return <Component {...props} />;
};
return Auth;
};
export default withAuth;
Sample of the use of withAuth
import React from "react";
import withAuth from "./withAuth";
function sample() {
return <div>This is a protected page</div>;
}
export default withAuth(sample);
you can make the authentication of user on server-side, if a user is logged in then show them the content of the protected route else redirect them to some other route. refer to this page for mote info.
in getServerSideProps check whether the user has logged in
if (!data.username) {
return {
redirect: {
destination: '/accounts/login',
permanent: false,
},
}
}
here's complete example of protected route page
export default function SomeComponent() {
// some content
}
export async function getServerSideProps({ req }) {
const { token } = cookie.parse(req.headers.cookie)
const userRes = await fetch(`${URL}/api/user`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
})
const data = await userRes.json()
// does not allow access to page if not logged in
if (!data.username) {
return {
redirect: {
destination: '/accounts/login',
permanent: false,
},
}
}
return {
props: { data }
}
}
With Customized 401 Page
We are going to first define our customized 401 page
import React from "react"
const Page401 = () => {
return (
<React.Fragment>
//code of your customized 401 page
</React.Fragment>
)
}
export default Page401
Now, we are going to change a small part of the code kiranr shared
export async function getServerSideProps({ req }) {
const { token } = cookie.parse(req.headers.cookie)
const userRes = await fetch(`${URL}/api/user`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
})
const data = await userRes.json()
// does not allow access to page if not logged in
if (!data.username) {
//THIS PART CHANGES
return {
props: {
unauthorized: true
}
}
//THIS PART CHANGES
}
return {
props: { data }
}
}
Then we will check this 'unauthorized' property in our _app.js file and call our customized 401 page component if its value is true
import Page401 from "../components/Error/Server/401/index";
const App = ({ Component, pageProps }) => {
//code..
if (pageProps.unauthorized) {
//if code block reaches here then it means the user is not authorized
return <Page401 />;
}
//code..
//if code block reaches here then it means the user is authorized
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
}
Related
Please refer to the code below:
Auth.tsx
import { createContext, useEffect, useState, useContext, FC } from 'react';
interface Props {
// any props that come into the component
}
export const PUBLIC_ROUTES = ['/', '/admin/login'];
export const isBrowser = () => typeof window !== 'undefined';
const AuthContext = createContext({
isAuthenticated: false,
isLoading: false,
user: {},
});
export const useAuth = () => useContext(AuthContext);
export const AuthProvider: FC<Props> = ({ children }) => {
const [user, setUser] = useState({});
const [isLoading, setLoading] = useState(true);
useEffect(() => {
async function loadUserFromCookies() {
// const token = Cookies.get('token');
// TODO: Get the token from the cookie
const token = true;
if (token) {
// console.log("Got a token in the cookies, let's see if it is valid");
// api.defaults.headers.Authorization = `Bearer ${token}`;
// const { data: user } = await api.get('users/me');
// if (user) setUser(user);
}
setLoading(false);
}
loadUserFromCookies();
}, []);
return (
<AuthContext.Provider
value={{ isAuthenticated: !!Object.keys(user).length, user, isLoading }}
>
{children}
</AuthContext.Provider>
);
};
export const ProtectRoute: FC<Props> = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <div>Loading</div>;
}
if (PUBLIC_ROUTES.includes(window.location.pathname)) {
return <>{children}</>;
}
// If the user is not on the browser or not authenticated
if (!isBrowser() || !isAuthenticated) {
window.location.replace('/login');
return null;
}
return <>{children}</>;
};
_app.tsx
import React from 'react';
import Head from 'next/head';
import { AppProps } from 'next/dist/next-server/lib/router/router';
import { ThemeProvider, StyledEngineProvider } from '#material-ui/core/styles';
import CssBaseline from '#material-ui/core/CssBaseline';
import theme from '../utils/theme';
import { AuthProvider, ProtectRoute } from 'contexts/auth';
export default function MyApp(props: AppProps) {
const { Component, pageProps } = props;
React.useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles && jssStyles.parentElement) {
jssStyles.parentElement.removeChild(jssStyles);
}
}, []);
return (
<React.Fragment>
<Head>
<title>Next App</title>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head>
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<AuthProvider>
<ProtectRoute>
<Component {...pageProps} />
</ProtectRoute>
</AuthProvider>
</ThemeProvider>
</StyledEngineProvider>
</React.Fragment>
);
}
Problems:
So, as per the code, if the user is not logged in, redirect the user
to the login page. But, due to the current logic, 404-page
routes are also redirected to the admin login page. How can I catch
the 404 status to redirect to the 404 pages before verifying if the
user is logged in or not?
I am using an array to verify if the path is public or not. Is there
a better way to render public paths without maintaining hard-coded
page paths or using HOCs? The issue with my approach is if any developer changes the file name and if it doesn't match the string in the array, it will not be a public route. If I create a folder called public and all public pages inside it, I get an unnecessary public/ in my URL. I do not want to use HOCs because I have to import them every time I create a new page which isn't incorrect but I am expecting a better approach.
Thank you.
You can use HOC (Higher Order Components) to wrap the paths which:
can only accessed with authorization;
can only be access when
user unauthenticated (e.g /login -> you don't want user to
access /login page when he's already logged in)
Like so:
login.js -> /login
import withoutAuth from 'path/withoutAuth';
function Login() {
return (<>YOUR COMPONENT</>);
};
export default withoutAuth(Login);
Same can be done with withAuth.js -> HOC (Higher Order Component) which wraps components which can only accessed with authentication
protectedPage.js -> /protectedPage
import withAuth from 'path/withAuth';
function ProtectedPage() {
return (<>YOUR COMPONENT</>);
};
export default withAuth(ProtectedPage);
You can how you can make these HOCs in this article.
Setting up authentication and withConditionalRedirect (HOC which will be used to make withAuth & withoutAuth HOCs): Detecting Authentication Client-Side in Next.js with an HttpOnly Cookie When Using SSR.
Making withAuth & withoutAuth HOCs: Detecting a User's Authenticated State Client-Side in Next.js using an HttpOnly Cookie and Static Optimization
Currently, I use the following PrivateRoute to determine if the user is logged in, and if so, the user is taken to the specified page, and if not, the user is taken to the login page. However, when I reload the page, it momentarily transitions to the login page and then to the root page, and I cannot display the /accounts or /notes page again.
This phenomenon also occurs when you type directly into the address bar.
If you know more about it, I would appreciate it if you could tell me why this kind of decrease is happening.
import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { connect } from 'react-redux'
const PrivateRoute = ({ component: Component, auth, ...rest }) => (
<Route
{...rest}
render={props => {
if (auth.isLoading) {
return <h2>Loading...</h2>;
} else if (auth.isAuthenticated) {
return <Component {...props} />;
} else {
return <Redirect to='/login' />;
}
}}
/>
);
const mapStateToProps = state => ({
auth: state.auth
})
export default connect(mapStateToProps)(PrivateRoute);
action
export const login = (username, password) => dispatch => {
const config = {
headers: {
'Content-Type': 'application/json',
}
};
const body = JSON.stringify({ username, password });
axios
.post(`${url}/api/auth/login`, body, config)
.then((res) => {
dispatch({
type: LOGIN_SUCCESS,
payload: res.data,
});
})
.catch((err) => {
dispatch(returnErrors(err.response.data, err.response.status));
dispatch({
type: LOGIN_FAIL,
});
});
};
The problem is that your auth state in redux will lost each time page reload so you can not identify authentication anymore. To prevent that your can save your state in localStorage or use redux-persist for it.
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>
)
}
Greetings Javascript Developers. I'm stuck in a complex situation now where I need to access a function inside one of my functinal components outside in a normal js file.
Ok So here's what I'm doing: This is my Authorizer.js functional Component.
import React, { createContext, useState, useEffect, useContext } from "react";
import SplashScreen from "react-native-splash-screen";
import { useStore } from "../config/Store";
import { useDatabase } from "../config/Persistence";
import { getSessionCredentials } from "../config/Persistence";
import NavigationDrawer from "./NavigationDrawer";
import AuthStacks from "./AuthStacks";
const AuthContext = createContext();
export const useAuthorization = () => useContext(AuthContext);
export function Authorizer() {
//TODO check whether user is already signed in or not.
const realm = useDatabase();
const { state, dispatch } = useStore();
const [isAuthorized, setAuthorization] = useState(false);
useEffect(() => {
VerifyCredentials();
}, []);
async function VerifyCredentials() {
//TODO Check from Async Storage?
var session = await getSessionCredentials();
console.log("saved session", session);
if (session) {
await DispatchShopData();
await setAuthorization(true);
} else {
await setAuthorization(false);
}
sleep(1000).then(() => {
SplashScreen.hide();
});
}
async function DispatchShopData() {
try {
let shop = await realm.objects("Shop");
await dispatch({ type: "UPDATE_SHOP_DETAILS", payload: shop[0] });
} catch (error) {
console.log("failed to retrieve shop object", error);
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
return (
<AuthContext.Provider value={{ setAuthorization }}>
{isAuthorized ? <NavigationDrawer /> : <AuthStacks />}
</AuthContext.Provider>
);
}
This component basically handles my Authentication Flow, whether to show the Navigation Drawer or the Login Screen. Now I have another simple javascript file ApiService.js which does not have any components, only simple js functions.
import Axios from "axios";
import { getAuthToken } from "../config/Persistence";
import { LogoutUser } from "../config/Persistence";
import { Alert } from "react-native";
const BASE_URL = "#########################";
/** Defined my Api Endpoints Here */
let service = Axios.create({
baseURL: BASE_URL,
timeout: 10000,
});
service.interceptors.response.use((response) => {
console.log("[API] response intercepted data", response.data.message);
if (!response.data.status && response.data.tokenExpired) {
//Auth token has Expired. Show user Alert for Session Expired & redirect to login screen.
Alert.alert(
"Your Session has Expired!",
"Don't worry though. You just need to login again & you're set.",
[
{
text: "Continue",
style: "default",
onPress: () => {
LogoutUser()
.then((success) => {
if (success) {
//TODO Find a way to Access this function from Authorizer.js Component.
//setAuthorization(false);
}
})
.catch((error) => {
console.log("failed to logout after session expiry", error);
});
},
},
]
);
}
return response;
});
/** Defined my other api functions called inside my other components */
function TestSampleApi() {
try {
return new Promise(async function (resolve, reject) {
const response = await service.get("https://jsonplaceholder.typicode.com/users");
if (response.data != null) {
resolve(response.data);
} else {
reject(response.status);
}
});
} catch (error) {
console.log("request error", error.message);
}
}
export {
TestSampleApi,
/** Exporting other api functions as well */
};
In my ApiService.js file, I've setup a response interceptors whose job is to catch the default auth token expired response and SignOut user immediately and take him to the Login Screen. Here's now where my issue comes.
In normal scenarios, where I need to access functions from one component inside another component, I can manage is using CreateContext() and useContent() hooks. However, how do I access the useState function setAuthorization in my Authorizer.js components in my ApiService.js file as a normal js function.
I only need to call setAuthorization(false) from my response interceptor block to make the user return to the Login Screen. Problem is idk how to access that state setter function. So any help would be greatly appreciated.
I am using the react-spotify-login package and when trying to authorize the application I can't retrieve the access token. My routing works and sending the request works. I just can't retrieve the token. I've just started learning react so I'm hoping it isn't something I'm easily overlooking.
import React, { Component } from 'react';
import SpotifyLogin from 'react-spotify-login';
import { clientId, redirectUri } from '../../Settings';
import { Redirect } from 'react-router-dom';
export class Login extends Component {
render() {
const onSuccess = ({ response }) => {
//const { access_token: token } = response;
console.log("[onSuccess]" + response);
return <Redirect to='/home' />
};
const onFailure = response => console.error("[onFailure]" + response);
return (
<div>
<SpotifyLogin
clientId={clientId}
redirectUri={redirectUri}
onSuccess={onSuccess}
onFailure={onFailure}
/>
</div>
);
}
}
export default Login;
In your approach you are trying to destructure the response data/object and pull field 'response' which does not exist i.e undefined
Change
const onSuccess = ({ response }) => {
to
const onSuccess = (response) => {