Trying to implement private ( authenticated ) routes in Nextjs using HOC and cookies but running into error below:
TypeError: Object(...) is not a function
at export default withPrivateRoute(Private);
I have checked elsewhere in the app that cookies are available and also sent with the request. They seem to be available server side.
The HOC at `/components/withPrivateRoute
import { withRouter } from 'next/router';
import { withCookies } from 'react-cookie';
const withPrivateRoute = (authComponent) => {
return class Private extends React.Component {
componentDidMount() {
console.log('PRIVATE ROUTE', this.props);
const { router, cookies } = this.props;
const intendedRoute = router.pathname;
const isAdmin = !!cookies.get('isAdmin');
const isAuthenticated = !!cookies.get('username');
if (!isAuthenticated) {
router.push({
pathname: '/login',
query: { from: intendedRoute },
});
}
if (
isAuthenticated &&
router.pathname.includes('admin') &&
!isAdmin
) {
router.push('/');
}
}
render() {
// eslint-disable-next-line react/jsx-props-no-spreading
return <authComponent {...this.props} />;
}
}
}
export default withCookies(withRouter(withPrivateRoute));
The private route example:
import withPrivateRoute from '../components/withPrivateRoute';
import getCategories from '../lib/getCategories';
const Private = (props) => {
console.log('props', props);
return <div>Private route </div>;
}
export default withPrivateRoute(Private);
export async function getStaticProps() {
let categories = await getCategories();
categories = categories.data.categories;
return {
props: {
categories,
},
};
}
I have since found a better way to handle private routes in Nextjs from this discussion:
Everything is handled inside getServerSideProps, no HOC required.
class Private extends React.Component{
render() {
console.log('props', this.props);
return <p>Private route</p>;
}
}
export default Private;
export async function getServerSideProps(context) {
const { req: { headers, url }, res } = context;
const cookies = {};
if (headers && headers.cookie) {
headers.cookie.split(';').forEach((cookie) => {
const parts = cookie.match(/(.*?)=(.*)$/);
cookies[parts[1].trim()] = (parts[2] || '').trim();
});
}
const isAuthenticated = !!cookies.username;
const isAdmin = !!cookies.isAdmin;
if (!isAuthenticated) {
res.setHeader('Location', `/login?from=${url}`);
res.statusCode = 307;
}
if (
isAuthenticated &&
url.includes('admin') &&
!isAdmin
) {
res.setHeader('Location', '/');
res.statusCode = 307;
}
return {
props: {
},
};
}
Related
Here my App.js
import React, { useEffect } from "react";
import { NavigationContainer } from "#react-navigation/native";
import AuthStore from "./src/stores/AuthStore";
import AuthStackNavigator from "./src/navigation/AuthStackNavigator";
import UnAuthStackNavigator from "./src/navigation/UnAuthStackNavigator";
const App = () => {
useEffect(() => {
console.log("APP JS", AuthStore.userAuthenticated);
}, [AuthStore.userAuthenticated]);
return <NavigationContainer>
{AuthStore.userAuthenticated ? <AuthStackNavigator /> : <UnAuthStackNavigator />}
</NavigationContainer>;
};
export default App;
The AuthStore value of userAuthenticated is computed and updated on auto login or login.
Here AuthStore.js
import { userLogin, userRegister } from "../api/AuthAgent";
import { clearStorage, retrieveUserSession, storeUserSession } from "../utils/EncryptedStorage";
import { Alert } from "react-native";
import { computed, makeObservable, observable } from "mobx";
import { setBearerToken } from "../config/HttpClient";
class AuthStore {
user = {};
token = undefined;
refreshToken = undefined;
decodedToken = undefined;
constructor() {
makeObservable(this, {
token: observable,
refreshToken: observable,
user: observable,
decodedToken: observable,
userAuthenticated: computed,
});
this.autoLogin();
}
async doLogin(body) {
const resp = await userLogin(body);
console.log("AuthStore > userLogin > resp => ", resp);
if (resp.success) {
this.decodedToken = await this.getDecodedToken(resp.token);
this.setUserData(resp);
storeUserSession(resp);
} else {
Alert.alert(
"Wrong credentials!",
"Please, make sure that your email & password are correct",
);
}
}
async autoLogin() {
const user = await retrieveUserSession();
if (user) {
this.setUserData(user);
}
}
setUserData(data) {
this.user = data;
this.token = data.token;
setBearerToken(data.token);
}
get userAuthenticated() {
console.log('AuthStore > MOBX - COMPUTED userAuthenticated', this.user);
if (this.token) {
return true;
} else return false;
}
async logout() {
await clearStorage();
this.user = undefined;
this.token = undefined;
this.refreshToken = undefined;
this.decodedToken = undefined;
}
}
export default new AuthStore();
The main problem is that the AuthStore.userAuthenticated value even when it changes on AuthStore it does not triggered by useEffect of the App.js.
So, when I log in or log out I have to reload the App to trigger the useEffect hook and then the navigators are only updated.
You can use useMemo hook to achive this.
const App = () => {
const [userToken, setUserToken] = useState("")
const authContext: any = useMemo(() => {
return {
signIn: (data: any) => {
AsyncStorage.setValue("token", data.accessToken);
setUserToken(data.accessToken);
},
signOut: () => {
setUserToken("");
AsyncStorage.setValue("token", "");
},
};
}, []);
return (
<AuthContext.Provider value={authContext}>
{userToken.length ? (
<UnAuthStackNavigator />
) : (
<AuthStackNavigator />
)}
)
</AuthContext.Provider>
)
}
AuthContext.ts
import React from "react";
export const AuthContext: any = React.createContext({
signIn: (res: any) => {},
signOut: () => {},
});
Now you can use this functions in any files like this:
export const SignIn = () => {
const { signIn } = useContext(AuthContext);
return (
<Button onPress={() => {signIn()}} />
)
}
If your primary purpose is to navigate in and out of stack if authentication is available or not then asynstorage is the best option you have to first
store token.
const storeToken = async (value) => {
try {
await AsynStorage.setItem("userAuthenticated", JSON.stringify(value));
} catch (error) {
console.log(error);
}
};
("userAuthenticated" is the key where we get the value )
Now go to the screen where you want this token to run turnery condition
const [token, setToken] = useState();
const getToken = async () => {
try {
const userData = JSON.parse(await AsynStorage.getItem("userAuthenticated"))
setToken(userData)
} catch (error) {
console.log(error);
}
};
Now use the token in the state then run the condition:
{token? <AuthStackNavigator /> : <UnAuthStackNavigator />}
or
{token != null? <AuthStackNavigator /> : <UnAuthStackNavigator />}
I have a React context which I am using to manage the authentication within my application. I have done this previously and all seemed OK, but in this application the value of the isAuthenticated property is not being updated. I've tried to replicate using CodeSanbox but I get the expected result.
Essentially, I want the context to hold a value of isAuthenticating: true until the authentication flow has finished, once this has finished I will determine if the user is authenticated by checking isAuthenticated === true && authenticatedUser !== undefined however, the state does not seem to be getting updated.
As a bit of additional context to this, I am using turborepo and next.js.
AuthenticationContext:
import { SilentRequest } from '#azure/msal-browser';
import { useMsal } from '#azure/msal-react';
import { User } from 'models';
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { msal, sendRequest } from 'utils';
interface AuthenticationContextType {
authenticatedUser?: User;
isAuthenticating: boolean;
}
const AuthenticationContext = createContext<AuthenticationContextType>({
authenticatedUser: undefined,
isAuthenticating: true
});
export const AuthenticationProvider = (props: { children: React.ReactNode }) => {
const { accounts, instance } = useMsal();
const [user, setUser] = useState<User>();
const [isAuthenticating, setIsAuthenticating] = useState<boolean>(true);
const [currentAccessToken, setCurrentAccessToken] = useState<string>();
const getUserFromToken = useCallback(async () => {
if (user) {
setIsAuthenticating(false);
return;
}
const userRequest = await sendRequest('me');
if (! userRequest.error && userRequest.data) {
setUser(userRequest.data as User);
}
}, [user]);
const getAccessToken = useCallback(async () => {
if (! currentAccessToken) {
const request: SilentRequest = {
...msal.getRedirectRequest(),
account: accounts[0]
}
const response = await instance.acquireTokenSilent(request);
setCurrentAccessToken(response.accessToken);
}
return getUserFromToken();
}, [accounts, currentAccessToken, getUserFromToken, instance]);
useEffect(() => {
async function initialiseAuthentication() {
await getAccessToken();
setIsAuthenticating(false);
}
initialiseAuthentication();
}, [getAccessToken]);
return (
<AuthenticationContext.Provider value={{ authenticatedUser: user, isAuthenticating }}>
{ props.children }
</AuthenticationContext.Provider>
)
}
export const useAuth = () => {
const context = useContext(AuthenticationContext);
if (context === undefined) {
throw new Error("useAuth was used outside of it's provider.")
}
return context;
}
AuthenticationLayout:
import { useEffect, useState } from 'react';
import { AuthenticationProvider, useAuth } from '../hooks/authentication';
import MsalLayout from './msal-layout';
const AuthenticationLayout = (props: { children: React.ReactNode }) => {
const { isAuthenticating, authenticatedUser } = useAuth();
const wasAuthenticationSuccessful = () => {
return ! isAuthenticating && authenticatedUser !== undefined;
}
const renderContent = () => {
if (! wasAuthenticationSuccessful()) {
return (
<p>You are not authorized to view this application.</p>
)
}
return props.children;
}
if (isAuthenticating) {
return (
<p>Authenticating...</p>
)
}
return (
<MsalLayout>
{ renderContent() }
</MsalLayout>
)
}
export default AuthenticationLayout;
MsalLayout:
import { InteractionType } from '#azure/msal-browser';
import {
AuthenticatedTemplate,
MsalAuthenticationTemplate,
MsalProvider,
} from "#azure/msal-react";
import { msalInstance, msal } from 'utils';
import { AuthenticationProvider } from '../hooks/authentication';
msal.initialize();
const MsalLayout = (props: { children: React.ReactNode }) => {
return (
<MsalProvider instance={msalInstance}>
<MsalAuthenticationTemplate interactionType={InteractionType.Redirect} authenticationRequest={msal.getRedirectRequest()}>
<AuthenticatedTemplate>
<AuthenticationProvider>
{props.children}
</AuthenticationProvider>
</AuthenticatedTemplate>
</MsalAuthenticationTemplate>
</MsalProvider>
)
}
export default MsalLayout;
Theoretically, once the authentication is finished I would expect the props.children to display.
I think that the problem is AuthenticationLayout is above the provider. You have consumed the provider in MsalLayout. Then AuthenticationLayout uses MsalLayout so the AuthenticationLayout component is above the provider in the component tree. Any component that consumes the context, needs to be a child of the provider for that context.
Therefore the context is stuck on the static default values.
Your capture of this scenario in useAuth where you throw an error is not warning you of this as when its outside the context -- context is not undefined, it is instead the default values which you pass to createContext. So your if guard isn't right.
There are some workarounds to checking if its available -- for example you could use undefined in the default context for isAuthenticating and authenticatedUser and then check that. Or you can change them to getters and set the default context version of this function such that it throws an error.
I trying to implement the AWS Amplify Authenticator in Ant Design Pro / UmiJS
In the UmiJS config.ts file I have this configuration:
routes: [
(...){
path: '/',
component: '../layouts/AwsSecurityLayout',
routes: [
{
path: '/',
component: '../layouts/BasicLayout',
authority: ['admin', 'user'],
routes: [
{
path: '/links',
name: 'fb.links',
icon: 'BarsOutlined',
component: './Links',
},(...)
The base component AwsSecurityLayout wraps the routes who will the guard.
Looks like that:
import React from 'react';
import { withAuthenticator } from '#aws-amplify/ui-react';
import { PageLoading } from '#ant-design/pro-layout';
class AwsSecurityLayout extends React.Component {
render() {
const { children } = this.props;
if (!children) {
return <PageLoading />;
}
return children;
}
}
export default withAuthenticator(AwsSecurityLayout);
Went I use the withAuthenticator func the props from UmiJs are not passed to the component, so props and props.child are all ways undefied.
The original file from Ant Design Pro:
import React from 'react';
import { PageLoading } from '#ant-design/pro-layout';
import { Redirect, connect, ConnectProps } from 'umi';
import { stringify } from 'querystring';
import { ConnectState } from '#/models/connect';
import { CurrentUser } from '#/models/user';
interface SecurityLayoutProps extends ConnectProps {
loading?: boolean;
currentUser?: CurrentUser;
}
interface SecurityLayoutState {
isReady: boolean;
}
class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
state: SecurityLayoutState = {
isReady: false,
};
componentDidMount() {
this.setState({
isReady: true,
});
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'user/fetchCurrent',
});
}
}
render() {
const { isReady } = this.state;
const { children, loading, currentUser } = this.props;
// You can replace it to your authentication rule (such as check token exists)
// 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在)
const isLogin = currentUser && currentUser.userid;
const queryString = stringify({
redirect: window.location.href,
});
if ((!isLogin && loading) || !isReady) {
return <PageLoading />;
}
if (!isLogin && window.location.pathname !== '/user/login') {
return <Redirect to={`/user/login?${queryString}`} />;
}
return children;
}
}
export default connect(({ user, loading }: ConnectState) => ({
currentUser: user.currentUser,
loading: loading.models.user,
}))(SecurityLayout);
I Just basically wrap the component using withAuthenticator
I solved using the conditional rendering doc: https://docs.amplify.aws/ui/auth/authenticator/q/framework/react#manage-auth-state-and-conditional-app-rendering
code:
import React from 'react';
import { AmplifyAuthenticator } from '#aws-amplify/ui-react';
import { AuthState, onAuthUIStateChange } from '#aws-amplify/ui-components';
const AwsSecurityLayout: React.FunctionComponent = (props: any | undefined) => {
const [authState, setAuthState] = React.useState<AuthState>();
const [user, setUser] = React.useState<any | undefined>();
React.useEffect(() => {
return onAuthUIStateChange((nextAuthState, authData) => {
setAuthState(nextAuthState);
setUser(authData);
});
}, []);
return authState === AuthState.SignedIn && user ? (
props.children
) : (
// TODO: change style / implement https://github.com/mzohaibqc/antd-amplify-react
<AmplifyAuthenticator />
);
}
export default AwsSecurityLayout;
In my App.tsx file I have this setup
const App: React.FC = props => {
const [hasRendered, setHasRendered] = useState(false);
const dispatch = useDispatch();
const isAuth = authMiddleWare();
useEffect(() => setHasRendered(true), [hasRendered]);
if (!hasRendered && isAuth !== null) {
if (isAuth) {
dispatch(getUser());
} else {
dispatch(logoutUser());
}
}
...
<PrivateRoute path="/app" component={Dashboard} />
}
const PrivateRoute = ({ component, location, ...rest }: any) => {
const dispatch = useDispatch();
const authenticated = useSelector((state: RootState) => state.user.authenticated);
return <>{authenticated ? React.createElement(component, props) : <Redirect to={{ pathname: LoginRoute, state: { from: props.location } }} </>;
};
However, authentication only gets executed on browser reload, so data is not updated if I click a link within my app. I want the user to be checked when loading any private route.
Create a separate auth component and call it in every route and check the user is authenticated or not.
if the user authenticated then requested route show else return the 404page or homepage.
or check in every page you can check your authentication by putting this code in componentditmount section
import React, { Component } from 'react'
import { connect } from "react-redux";
import { userLoginAction } from "./store/actions/userLogin";
import * as actionType from "./store/actions/actionType";
import { Redirect } from 'react-router-dom';
class componentName extends Component {
constructor(props){
super(props)
this.state={
isMount:false
}
}
componentDidMount(){
const token = localStorage.getItem("token");
if (token != null && this.props.loginStatus === false) {
// this.userLoginData(token)
this.props.auth();
}
this.setState({ isMount: true });
}
render() {
return (
<>
{this.state.isMount?this.props.auth===false?<Redirect from ='/' to='/login' />:
<p>Hello World!</p> :<div>Loging.....</div>
}
</>
)
}
}
const mapGetState = (state) => {
return {
loginStatus: state.usrReducer.login_st,
auth: state.usrReducer.auth,
};
};
const mapDispatchState = (dispatch) => {
return {
login: (data) => dispatch({ type: actionType.LOGIN_ST, payload: data }),
auth: (data) => dispatch(userLoginAction(data)),
};
};
export default connect(mapGetState, mapDispatchState)(componentName)
Here is my _app.js page:
import { Provider } from 'react-redux';
import App from 'next/app';
import withRedux from 'next-redux-wrapper';
import { Layout } from '../components';
import makeStore from '../store';
import { api } from '../utils/api';
class MyApp extends App {
render() {
const { Component, pageProps, store } = this.props;
console.log(this.props); // Here I cannot see cookie user data, it is empty object even though the cookie is set in `_document.js`
return (
<Provider store={store}>
<Layout { ...this.props }>
<Component { ...pageProps } />
</Layout>
</Provider>
);
}
}
MyApp.getInitialProps = api.authInititalProps();
export default withRedux(makeStore)(MyApp);
As you can see I'm trying to get cookie user data in getInitialProps lifycycle. Here my api file which gathers the logic of getting the cookie data:
const WINDOW_USER_SCRIPT_KEY = '__USER__';
class API {
getServerSideToken = req => {
const { signedCookies = {} } = req;
if(!signedCookies || !signedCookies.token) {
return {}
}
return { user: signedCookies.token };
}
getClientSideToken = () => {
if(typeof window !== 'undefined') {
const user = window[WINDOW_USER_SCRIPT_KEY] || {};
return { user };
}
return { user: {} }
};
authInititalProps = () => ({ req, res }) => {
return req ? this.getServerSideToken(req) : this.getClientSideToken();
}
}
The token itself is set in `_document.js:
import Document, { Head, Main, NextScript } from 'next/document';
import { api } from '../utils/api';
class MyDocument extends Document {
static async getInitialProps(ctx) {
const props = await Document.getInitialProps(ctx);
const userData = await api.getServerSideToken(ctx.req);
return { ...props, ...userData }
}
render() {
const { user = {} } = this.props;
return(
<html>
<Head />
<body>
<Main />
<script dangerouslySetInnerHTML={{ __html: api.getUserScript(user) }} />
<NextScript />
</body>
</html>
);
}
};
export default MyDocument;
What is happening is that I'm not getting the cookie token inside my _app.js, instead I'm getting empty object, even though the cookie is set and in the console after typing window.__USER__ I can see it. The aim of all that is to pass user data down to Layout component which is invoked inside the _app.js. Why cannot I see it inside _app.js?