So, I'm fetching data from an API that is built in Liferay (json web service) and in which I'm to integrate into a React frontend. Right now I'm at the stage where I need to authenticate a user, and I'm halfway there. I'm currently sending a post-request with the Fetch API, to a specific endpoint that is defined in the backend. From there I get back some user data in JSON format, but also a JSESSIONID cookie. I'm mostly used to using json web tokens for authentication and basically what I need to know how to use this session cookie in a similar way.
I'm thinking in my mind I want to store the cookie in a state/context, and let the application determine if there is a session, and only then be able to access protected routes, etc, but I'm not entirely sure this is the way you do it.
I would really be glad if someone could point me in the right direction.
The context to verify that the user is authenticated.
import * as React from 'react'
export const AuthContext = React.createContext({})
const AuthProvider = ({
children
}) => {
const [dataUser, setDataUser] = React.useState({})
const Data = async () => {
const user = await fetch('url')
const resp = await user.json()
setDataUser(resp)
//cookies or localStorage
localStorage.setItem('user', resp)
}
React.useEffect(() => {
Data()
}, [])
const existUser = () => {
return JSON.parse(localStorage.user).id === dataUser.id
}
const Auth = existUser()
return(
<AuthContext.Provider value={{dataUser, setDataUser, Auth}}>
{children}
</AuthContext.Provider>
)
}
export default AuthProvider
put the provider in the app
const App = () => {
return(
<AuthProvider>
<Pages />
</AuthProvider>
)
}
export default App
build the pages that need authentication, using useContext, or a custom react hook.
const DashBoard = ({}) => {
const { Auth } = React.useContext(AuthContext)
const isAuth = () => {
Auth ?? window.location('url') // or react router history
}
React.useEffect(() => {
isAuth()
}, [])
return(
<>
{Auth && //optional
<div>
<h1>DashBoard</h1>
</div>
}
</>
)
}
Related
This is my first real mobile app and when I am trying to implement auth and routing I am running into some issues - both error message and I am guessing functional too
My app currently has two stacks, an auth stack, and a drawer stack. I have the auth stack as the default stack and want to display the drawer stack if the user is logged in. If they are logged out show them the auth stack till they login.
I have this line of code in my root stack navigator
{ auth ? <Stack.Screen name="Auth" component={AuthStack} />:<Stack.Screen name="Drawer" component={DrawerStack} />}
Above my stack navigator I have this line
const { auth } = checkAuth()
Which is imported using - correct file path
import { AuthProvider, checkAuth } from '../context/AuthContext'
The base code from that import is below
const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState(null);
const checkAuth = () => {
try {
const authData = globalStorage.getString('accessToken')
if(authData !== null && authData !== undefined) {
setAuth(authData)
}
} catch(e) {
console.error(e)
}
}
const removeAuth = () => {...};
const setAuthState = data => {
try {
console.log('setAuthState Data: ', data)
globalStorage.set('accessToken', data)
setAuth(data);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
checkAuth();
}, []);
return (
<AuthContext.Provider value={{ auth, setAuthState, removeAuth}}>
{children}
</AuthContext.Provider>
);
};
The error message I am seeing in the iOS simulator is that checkAuth is not a function. I am not sure why it isn't when I am doing the import. I tried adding the AuthProvider as a prepend but no luck. I am sure this is a simple React thing but I'm not sure as I don't normally code this way when I do Node.js work.
Edit
import { AuthProvider, checkAuth } from '../context/AuthContext'
...
const AppNavigation = () => {
return (
<AuthProvider> <-- Error on this line
<RootNavigator />
</AuthProvider>
);
};
...
Error message
undefined is not an object (evaluating '_react.React.createElement')
checkAuth is scoped to AuthProvider. You'll want to include it in the value prop of AuthContext.Provider if you want make it available to consumers of your AuthContext, but I'd imagine you'll just want to use auth which is already provided to consumers.
Here is some info on the useContext hook where you can access those properties. https://reactjs.org/docs/hooks-reference.html#usecontext
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])
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
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
};
I am using a custom NextJS server with Apollo Client. I want to fetch the GraphQL data server-side and then send it to the client. I was kind of able to do that, but the client-side fetches it again. I understand that the Apollo cache is available only on the server, then needs to be sent to the client and restored from there.
The Apollo docs mention SSR but I don't want to fully render my app using the Apollo client, I want to use NextJS, I want to get just the data from the Apollo client and manually inject it into the HTML to restore it on the client. I looked at some examples for NextJS using Apollo, but none of them showed how to do exactly that.
This is my custom handler for requests:
const app = next({ dev: process.env.NODE_ENV !== 'production' });
const customHandler = async (req, res) => {
const rendered = await app.renderToHTML(req, res, req.path, req.query);
// somehow get the data from the apollo cache and inject it in the rendered html
res.send(rendered);
}
When you create the ApolloClient in the server, you can pass the initialState to hydrate the cache.
const createApolloClient = ({ initialState, headers }) =>
new ApolloClient({
uri: GRAPHQL_URL,
cache: new InMemoryCache().restore(initialState || {}) // hydrate cache
});
export default withApollo(PageComponent, { ssr = true } = {}) => {
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
const client = apolloClient || createApolloClient({ initialState: apolloState, headers: {} });
... rest of your code.
});
});
I have created a package just for this called nextjs-with-apollo. Take a look at https://github.com/adikari/nextjs-with-apollo. Once you have installed the package create a HOC as such.
// hocs/withApollo.js
import withApollo from 'nextjs-with-apollo';
import ApolloClient from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
const GRAPHQL_URL = 'https://your-graphql-url';
const createApolloClient = ({ initialState, headers }) =>
new ApolloClient({
uri: GRAPHQL_URL,
cache: new InMemoryCache().restore(initialState || {}) // hydrate cache
});
export default withApollo(createApolloClient);
Then you can use the hoc for your next page as such.
import React from 'react';
import { useQuery } from '#apollo/react-hooks';
import withApollo from 'hocs/withApollo';
const QUERY = gql`
query Profile {
profile {
name
displayname
}
}
`;
const ProfilePage = () => {
const { loading, error, data } = useQuery(PROFILE_QUERY);
if (loading) {
return <p>loading..</p>;
}
if (error) {
return JSON.stringify(error);
}
return (
<>
<p>user name: {data.profile.displayname}</p>
<p>name: {data.profile.name}</p>
</>
);
};
export default withApollo(ProfilePage);