How to keep or resupply React Context in a Gatsby site - reactjs

I use React Context API to store the information that a user is authenticated.
In development mode when I type in any URL that redirects to the 404 error page the context data is lost. When I navigate to a valid page a previously logged in user is not logged in any more.
EDIT: I just tested this with gatsby build and gatsby serve. A built gatsby site keeps the context when redirecting to 404 error page. But the context is still lost when navigating to completely different URL such as www.google.com.
Now my question is: How do I resupply the context with the login information without having the user be manually log in again?
Here is my AuthContextProvider wrapper class:
export class AuthContextProvider extends React.Component {
constructor(props) {
super(props);
this.state = { user: {} };
}
// ...
render() {
return (
<AuthContext.Provider value={{ getUser: this.getUser, setUser: this.setUser }}>
{this.props.children}
</AuthContext.Provider>
);
}
}
I wrap my whole app with the Context Provider in a root layout:
const RootLayout = ({ children }) => {
return (
<AuthContextProvider>
{children}
</AuthContextProvider>
);
}

React Context is about providing some data to one or more child components without having to pass the data down through intermediary components. There's no built-in mechanism for persisting state between page loads, so you'll need to reach for another tool for that.
If you haven't already implemented your authentication layer, you'll want to look into how that will work. There are a number of strategies for maintaining that state, even just within using cookie-based storage. JWT (JSON Web Token) are a popular method that will let you store signed user and client-readable data in the cookie at the cost of requiring a bit more work to manage expiration/renewal and having a larger payload. Assuming that's the approach you took, you might do something like this:
import React from "react";
import jwt from "jsonwebtoken"; // Add jsonwebtoken via npm/yarn
function getCookieValue(a) {
var b = document.cookie.match('(^|[^;]+)\\s*' + a + '\\s*=\\s*([^;]+)');
return b ? b.pop() : '';
}
const AUTH_PUBLIC_KEY = "your JWT public key here"
export const AuthContext = React.createContext();
export class AuthContextProvider extends React.Component {
state = {
authenticated: false,
userid: null,
};
componentDidMount() {
jwt.verify(getCookieValue("session"), AUTH_PUBLIC_KEY, (err, session) => {
if (!err && session.userid) {
this.setState({ userid: session.userid, authenticated: true })
}
})
}
// Important: REMOVE THIS AFTER TESTING/DEV
toggleLogin = () => {
this.setState(state => ({
authenticated: !state.authenticated,
userid: 2,
}));
}
render() {
return (
<AuthContext.Provider
value={{
...this.state,
toggleLogin: this.toggleLogin,
}}
>
{this.props.children}
</AuthContext.Provider>
);
}
}
This will parse the JWT token in the session cookie when the AuthContextProvider is mounted and update the state with the userid value stored in the JWT if one is present.
You will probably want to wrap the Gatsby App with this component, which you can do from gatsby-browser.js and gatsby-ssr.js files (create them in the root of your repo if you don't have them yet):
// gatsby-browser.js
import React from "react"
import AuthContextProvider from "components/AuthContextProvider"
export const wrapRootElement = ({ element }) =>
<AuthContextProvider>{element}</AuthContextProvider>
// gatsby-ssr.js
import React from "react"
export { wrapRootElement } from "./gatsby-browser"
You will still need to handle generating the JWT token (probably from a backend that is handling authentication) and if it's not already being persisted in a cookie you can access from the browser you will need to handle creation of that cookie at the relevant point in your application lifecycle.

I hope this helps you or others. The blog post below describes how you need to use gatsby-browser.js to wrap the root element in the provider so that it doesn't reset it on page change.
https://www.gatsbyjs.org/blog/2019-01-31-using-react-context-api-with-gatsby/

You have 3 possibilities:
web storage aka localStorage or sessionStorage (easiest, least secure)
session cookies (secure, requires backend server)
json web tokens (JWT) (most secure, requires backend server)
An excellent read about background infromation is this blog on dev.to.
1. web storage such as localStorage
This is considered to be the least secure option secure option. Do not save personal data such as email adresses here. Never ever save sensitive information such as credit card information and such.
This question describes how to use it:
var testObject = { 'one': 1, 'two': 2, 'three': 3 };
// Put the object into storage
localStorage.setItem('testObject', JSON.stringify(testObject));
// Retrieve the object from storage
var retrievedObject = localStorage.getItem('testObject');
console.log('retrievedObject: ', JSON.parse(retrievedObject));
2. cookies or session cookies
For Express you can use express-ession. Other web servers have similar middleware. The point is to supply the user info within a cookie as described on MDN.
3. json web tokens
This is similar to cookies but uses JSON web tokens. #coreyward gave an excellent answer. You can also read more in this blog post.

Related

Keycloak acts as if its uninitialized after a hard page refresh

First of all, thanks in advance to anyone who reads my question and comments. I have a CRA app that is using keycloak-js and ReactKeycloakProvcer from `#react-keycloak/web. When you first load the application page and login, keycloak is initialized correctly and functions as it should. The provider gets an instance of KC in a pretty standard way.
import keycloak from './authentication/keycloak'
const KeycloakProviderBlock = ({children}) => {
return (
<ReactKeycloakProvider authClient={keycloak} initOptions={{onLoad: 'login-required'}}>
{children}
</ReactKeycloakProvider>
);
};
Later in my axios wrapper, I am pulling the KC token out to add to all requests as the bearer token like this:
import keycloak from "./authentication/keycloak";
const {authenticated} = keycloak;
if (authenticated) {
client.defaults.headers.common = {
...client.defaults.headers.common,
Authorization: `Bearer ${keycloak.token}`,
};
} else {
logger.error("Request client used before KeyCloak initialized");
}
My keycloak file just returns a new instance of KC --> contents of /authentication/keycloak.js
import Keycloak from "keycloak-js";
const keycloak = new Keycloak({
realm: process.env.REACT_APP_KEYCLOAK_REALM,
url: process.env.REACT_APP_KEYCLOAK_URL,
clientId: process.env.REACT_APP_KEYCLOAK_CLIENT,
})
export default keycloak
Everything works like it should until the user hard refreshes the page. When the page reloads keycloak.authenticated is not present on the KC object, so all HTTP calls fail because there is no Bearer token.
I'm using keycloak-js version 15.0.2. Any/all thoughts appreciated.
I figured out how to fix this and figured I would post the answer in case it helps anyone else. It turns out when the page refreshes, it takes KC longer to restore the session information from the cookie than it does for the code to run. So there was a race condition when the page reloaded it was trying to reach the backend before KC had a chance to validate the user was indeed logged in. It turns out that the KeycloakProvider emits events and tells you when the token is refreshed. The events were not firing because I had the KCProvider wrapped in a JSX component, so the events were not properly bound. I removed the unnecessary block and the events started to fire. From there, it was pretty easy to display a Loading throbber and block the rest of the components from rendering until the provider actually got the onReady event. The new code looks like this:
In App.js
// Keycloak
onKeycloakEvent = (event, error) => {
console.log('onKeycloakEvent', event, error)
console.log(`Keycloak Event ${event}`);
if(event && event === 'onReady'){
this.setState({keycloakReady: true})
}
}
onKeycloakTokens = (tokens) => {
console.log('onKeycloakTokens', tokens)
}
...
In the Render Pass the functions to the provider:
<ReactKeycloakProvider authClient={keycloak} initOptions={{onLoad: 'login-required'}}
keycloak={keycloak}
onEvent={this.onKeycloakEvent}
onTokens={this.onKeycloakTokens}>
<SplashScreen keycloakReady={keycloakReady}>
....
</SplashScren>
</ReactKeycloakProvder>
Then in the SplashScreen only render children if KC is ready:
import React, {Component, PureComponent, ReactChildren} from "react";
import LoadingSpinner from "../navigation/LoadingSpinner";
type PassedProps = {
keycloakReady: boolean;
}
class SplashScreen extends PureComponent<PassedProps, any> {
constructor(props: PassedProps) {
super(props);
}
render() {
console.log(`SplashScreen keycloak ready ${this.props.keycloakReady}`);
if(!this.props.keycloakReady){
return <LoadingSpinner/>
}else{
return this.props.children
}
}
}
export default SplashScreen

Fetch data after login

I have a login component:
const login = async (e) => {
e.preventDefault();
const {data} = await loginUserRes({variables: {
...formData,
}});
if (data) {
currentUserVar({
...data.loginUser,
isLoggedIn: true,
});
}
};
Which allows user to send some data to the server, if the credentials are correct a JWT is stored in a cookie for authentication.
I'm wondering what he best approach is to fetching data after a log in, on the dashboard I have a list of movies that the user has added. Should I use a react effect to react to the currentUserVar changing and then do request for the movies? Or perhaps I could add all the dashboard data to the return object of the apollo server, that way the data is present on login.
Yes you should react to your authorization data, But this info always considered as global state that you will need it in the whole app. so It is recommended to save your currentUserVar in a react context or whatever you are using to handle global state, Even if the app was small and you don't have global state then you can left it to your root component so you can reach this state in all your components.
Here how I handle auth in reactjs, I don't know if it is the better approach but I'm sure it is a very good solution:
Create a wrapper component named AuthRoute that checks if the user is logedin or
not.
Wrap all components that require user auth with AuthRoute
If we have user data then render wrapped component.
If there is no user data then redirect to Login component
const AuthRoute = ({component, children, ...rest}) => {
const Component = component;
const {globalState} = useContext(GlobalContext);
// Return Component if user is authed, else redirect it to login page
return (
<Route
{...rest}
render={() =>
!!globalState.currentUser ? ( // if current user is exsits then its login user
children
) : (
<Redirect
to={ROUTES_PATH.SIGN_IN.index}
/>
)
}
/>
);
};
Then any component can fetch the needed data in it's mount effect useEffect(() => { fetchData() }, []). and this is an indirect react to userData.
Note: if you are not using routing in your app, you still can apply this solution by converting AuathRoute component to HOC.

How can I stay the user in the same page?

Every time I reload the my account page, it will go to the log in page for a while and will directed to the Logged in Homepage. How can I stay on the same even after refreshing the page?
I'm just practicing reactjs and I think this is the code that's causing this redirecting to log-in then to home
//if the currentUser is signed in in the application
export const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged(userAuth => {
unsubscribe();
resolve(userAuth); //this tell us if the user is signed in with the application or not
}, reject);
})
};
.....
import {useEffect} from 'react';
import { useSelector } from 'react-redux';
const mapState = ({ user }) => ({
currentUser: user.currentUser
});
//custom hook
const useAuth = props => {
//get that value, if the current user is null, meaning the user is not logged in
// if they want to access the page, they need to be redirected in a way to log in
const { currentUser } = useSelector(mapState);
useEffect(() => {
//checks if the current user is null
if(!currentUser){
//redirect the user to the log in page
//we have access to history because of withRoute in withAuth.js
props.history.push('/login');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},[currentUser]); //whenever currentUser changes, it will run this code
return currentUser;
};
export default useAuth;
You can make use of local storage as previously mentioned in the comments:
When user logs in
localStorage.setItem('currentUserLogged', true);
And before if(!currentUser)
var currentUser = localStorage.getItem('currentUserLogged');
Please have a look into the following example
Otherwise I recommend you to take a look into Redux Subscribers where you can persist states like so:
store.subscribe(() => {
// store state
})
There are two ways through which you can authenticate your application by using local storage.
The first one is :
set a token value in local storage at the time of logging into your application
localStorage.setItem("auth_token", "any random generated token or any value");
you can use the componentDidMount() method. This method runs on the mounting of any component. you can check here if the value stored in local storage is present or not if it is present it means the user is logged in and can access your page and if not you can redirect the user to the login page.
componentDidMount = () => { if(!localStorage.getItem("auth_token")){ // redirect to login page } }
The second option to authenticate your application by making guards. You can create auth-guards and integrate those guards on your routes. Those guards will check the requirement before rendering each route. It will make your code clean and you do not need to put auth check on every component like the first option.
There are many other ways too for eg. if you are using redux you can use Persist storage or redux store for storing the value but more secure and easy to use.

NextJS App with Auth0 and Redux failing to retrieve Auth session on manual refresh

Problem: if I refresh a page, getInitialProps trigger but it gets me the error describe on the bottom of the page which points out an issue going all the way to my redux wrapper on /_app.js, which I believe to be the issue. This behavior only happens on refresh page. If I go from a page that has no getInitialProps and then navigate client side to the page with getInitialProps it works fine. If I then do a manual refresh of the page it triggers the error.
This is my setup for redux and I've tried it with the Class based version with same results so that's not the issue.
/_app.js
import { Provider } from "react-redux";
import withRedux from "next-redux-wrapper";
import { initStore } from "../store";
import "./styles.scss";
const MyApp = props => {
const { Component, pageProps, store } = props;
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
};
MyApp.getInitialProps = async ({ Component, ctx }) => {
const pageProps = Component.getInitialProps
? await Component.getInitialProps(ctx)
: {};
return { pageProps };
};
export default withRedux(initStore)(MyApp);
this is the getInitialProps on the index.js page
/pages/index.js
OpenApplications.getInitialProps = async ({ store }) => {
const stateDB = await getApplications();
// populate state with all applications from DB
await store.dispatch(populateApplicationsState(stateDB));
return store;
};
this is the internal API setup with Auth
/api/applications.js
import auth0 from "../../../../lib/auth/auth0";
// auth0.requireAuthentication ensures that only authenticated users access this internal API path
export default auth0.requireAuthentication(async (req, res) => {
// get user profile from Auth0 on the server side
const { user } = await auth0.getSession(req);
process the rest of the stuff...
})
this is the code behind the Auth0 init config
.../auth/auth0.js
import { initAuth0 } from "#auth0/nextjs-auth0";
import config from "./config";
export default initAuth0({
clientId: config.AUTH0_CLIENT_ID,
clientSecret: config.AUTH0_CLIENT_SECRET,
scope: config.AUTH0_SCOPE,
domain: config.AUTH0_DOMAIN,
redirectUri: config.REDIRECT_URI,
postLogoutRedirectUri: config.POST_LOGOUT_REDIRECT_URI,
session: {
cookieSecret: config.SESSION_COOKIE_SECRET,
cookieLifetime: config.SESSION_COOKIE_LIFETIME,
cookieSameSite: "lax",
storeIdToken: true,
storeRefreshToken: true,
storeAccessToken: true
}
});
This is the Application tab on Chrome Dev Tools and it shows the a0state and a0session. When I refresh, the state disappears but the cookie stays. On an unprotected page it then comes back after I assume making a call to check the session is valid but on a protected page it goes into the error above.
error if requireAuthtentication is taken OFF
error if requireAuthentication is ON
So my suspicion is that whenever a manual page refresh is onset, auth0 loses the state and it doesn't pass it in the req to the API so that it fails the auth check and also it fails to get the session from the user. If I remove the auth0.requireAuthentication, it still fails because I'm dependent on the user which comes from auth0.getSession(req); which fails nonetheless and that triggers an error anyway because user comes undefined. What I think it's happening is that the session is not being passed on correctly due to the Redux wrapper setup that doesn't pass the necessary info or triggers the necessary Auth for that info to be there. At this point I'm a bit lost as all these tools are new to me and I'm not sure where it's going wrong and where to fix it and since it's 3 different libraries interacting it gets even harder. Help figure this super annoying bug :)
I checked everywhere and couldn't find a solution or a similar setup to get a comparison and find out a potential mistake I've done on my setup. Everything is pretty much like the examples on the docs for the different tools next-wrapper-redux/nextjs-auth0/next

Adding reset password functionality to react / redux login functionality

I used the following login with react/redux tutorial to build a signup / signin functionality into my React app, however I did not realize until recently that I now also need a reset-password / forgot-password functionality.
This feature is not a part of the tutorial at all, and I am simply wondering if anybody has any suggestions as to how I can go about this?
Let me know if I can share any info about my app that will help with this, or if there's a better place to post this type of question. I'm holding off on sharing more on the app as I think it's redundant given the info in the tutorial is nearly exactly how my signup / signin is setup.
Thanks!
After the user enters the proper credentials that you state (usually username, email, or both)
Make an api call to your backend that creates a password reset token. Store it in the database and, in one form or another, associate it with the user (usually it's the same database entry).
Send an email to the user with a link that has the password reset token embedded into it. Have a route in your react-router routes that will handle the url you link to.
Have the route mount a component that has a componentDidMount, which takes the token and makes an api to the backend to validate the token.
Once validated, open a ui element in the react component that allows the user to set a new password
Take the new password, password confirmation, and reset token and make an api call to the backend to change the password.
Delete the reset token in the backend after successful password change
// in your routes file
<Route path="/password_reset/:token" component={PasswordResetValidator}/>
//
class PasswordResetValidator extends React.Component {
state = {password: '', passwordReset: '', isValidated: false}
async componentDidMount() {
const response = await validatePasswordResetToken(this.props.token)
if (response.ok) {
this.setState({ isValidated: true })
} else {
// some error
}
}
handleSubmit = () => {
const { token } = this.props
const { password, passwordReset } = this.state
sendPasswordResetData({password, passwordReset, token})
// probably want some confirmation feedback to the user after successful or failed attempt
}
render() {
if(this.state.isValidated) {
return (
<div>
<input />
<input />
<button onClick={this.handleSubmit}>Set new password</button>
<div>
)
}
return // something while token is being validated
}
}
Obviously you need to make your own text input handlers. You should also have error handling, and good ui feedback to the user. But ultimately, that's all at your discretion.
Best of luck

Resources