I am building an application which uses authorization with Json Web Tokens. I'm building this application with Node.js, GraphQL and Apollo client V2 (and some other stuff, but those aren't related here). I have created a login resolver and a currentUser resolver that let me get the current user via a JWT. I later use that token and send it in my authorization headers and the results looks something like:
So that part is done! But here's what I'm having trouble with.
Me trying to explain the situation
I'm using React for the frontend part of this project with the Apollo Client V2. And when I do the login mutation I do it like this. With formik I've created my onSubmit:
const response = await mutate({
variables: {
email: values.email,
password: values.password,
},
})
const token = response.data.login.jwt
localStorage.setItem('token', token)
history.push('/') // Navigating to the home page
And this is what I want back with the login mutation (just the token):
export const loginMutation = gql`
mutation($email: String!, $password: String!) {
login(email: $email, password: $password) {
jwt
}
}
`
To get the currentUser's data I've put my currentUser query in my root router file. Please apologize me for naming the component PrivateRoute. I haven't renamed it yet because I can't find a proper name for it. I'm sorry. So in /src/router/index.js I have this:
// First the actual query
const meQuery = gql`
{
currentUser {
id
username
email
}
}
`
...
// A component that passess the currentUser as a prop, otherwise it will be null
const PRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={props => {
return (
<Component
{...props}
currentUser={
rest.meQuery.currentUser ? rest.meQuery.currentUser : null
}
/>
)
}}
/>
)
}
// Another component that uses the meQuery so I later can access it if I use the PrivateRoute component.
const PrivateRoute = graphql(meQuery, { name: 'meQuery' })(PRoute)
// Using the PrivateRoute. And in my Home component I can later grap the currentUser via propsb
const App = () => (
<Router>
<div>
<PrivateRoute exact path="/" component={Home} />
...
In the Home component I grab the prop:
const { data: { loading, error, getAllPosts = [], currentUser } } = this.props
I pass it down to my Navbar component:
<Navbar currentUser={this.props.currentUser} />
And in the Navbar component I take the username if it exists:
const { username } = this.props.currentUser || {}
And then I render it.
This is what I'm having troubles with
My application is currently trying to get the currentUser when I get to the /login route. And after I've successfully loged in I get back the token, but the currentUser query is not being fetched again. Thefore I have to refresh my page to get the current user and all of it's values.
I have also created a little video that demonstrates what my problem is. I believe it will show you more clearly the problem than me trying to type it.
Here's the video: https://www.youtube.com/watch?v=GyI_itthtaE
I also want to thank you for reading my post and that you hopefully are going to help me. I have no idea why this is happening to me and I just can't seem to solve it. I've tried to write this question as best as I can, so sorry if it was confusing to read.
Thanks
I think you might be able to solve this issue by setting the query's FetchPolicy to "cache-and-network". You can read about fetch policies here: "GraphQL query options.fetchPolicy"
in your specific case I think you can update this line
const PrivateRoute = graphql(meQuery, { name: 'meQuery' })(PRoute)
to this:
const PrivateRoute = graphql(meQuery, { name: 'meQuery', options: {fetchPolicy: 'cache-and-network'} })(PRoute)
Explanation
As stated in the documentation, the default policy is cache-first.
currentUser is queried the first time and it updates the cache.
You execute the login mutation, the cache is not updated without
you updating it (read about it here: "Updating the cache after a
mutation").
currentUser query is executed again but due to the default cache-first policy the outdated result will be retrieved only from the cache.
From the official documentation:
cache-first: This is the default value where we always try reading
data from your cache first. If all the data needed to fulfill your
query is in the cache then that data will be returned. Apollo will
only fetch from the network if a cached result is not available. This
fetch policy aims to minimize the number of network requests sent when
rendering your component.
cache-and-network: This fetch policy will have Apollo first trying to read data from your cache. If all the data needed to fulfill your
query is in the cache then that data will be returned. However,
regardless of whether or not the full data is in your cache this
fetchPolicy will always execute query with the network interface
unlike cache-first which will only execute your query if the query
data is not in your cache. This fetch policy optimizes for users
getting a quick response while also trying to keep cached data
consistent with your server data at the cost of extra network
requests.
in addition to these two there are two more policies: 'network-only' and 'cache-only' here's the link to the documentation
for me it worked when I refetched the currentUser query in my login mutation. I added my code below. Maybe it helps:
onSubmit(event) {
event.preventDefault();
const { email, password } = this.state;
this.props
.mutate({
variables: { email, password },
update: (proxy, { data }) => {
// Get Token from response and set it to the localStorage
localStorage.setItem('token', data.login.jwt);
},
refetchQueries: [{ query: currentUser }]
})
.catch((res) => {
const errors = res.graphQLErrors.map(error => error.message);
this.setState({ errors });
});
}
Related
I've build a NextJS blog and want to write backend with NodeJS/Express for that blog to run in my local and then my own server.
And I wanted to use MongoDB/Mongoose to fetch blog posts data in my local.
I've created [slug].js file to show single blog post in my /posts/ directory.
In my index.js pages, I could achieve fetching all the posts data from my local and show all the blog titles without an error. So getStaticProps function working correctly with my mongoose.
But the problem is when i click the single post link in index page, i can't go to single blog post.When i click the link, nothing happens.
Also if i make a get request to single post page from postman application. No response is coming.
Instead of using my own database, i put some data top of the [slug].js page and i could use that data as a db succesfully.
Also if i copy the getStaticProps or getStaticPaths function from [slug].js file to another file to test. I can still receive the data from db.
Only in [slug].js file and only when i use my local db; single post page does not working.
Here is my [slug].js file:
import PostModel from "../../backend/models/PostModel";
export async function getStaticPaths() {
// Return a list of possible value for slug
const allPostData2 = await PostModel.find();
const paths = allPostData2.map((post) => {
return { params: { slug: post.slug } };
});
return {
paths,
fallback: false,
};
}
export async function getStaticProps(context) {
// Fetch necessary data for the blog post using params.slug
const slug = context.params.slug;
const myPost2 = await PostModel.find({ slug: slug });
return {
props: {
post: myPost2,
},
};
}
export default function PostPage({ post }) {
return (
<div>
<div>{myPost.title}</div>
<div>This is a single post page</div>
</div>
);
}
Here is the github repo for the project,any help would be greatly appreciated:
https://github.com/hakanolgun/try-next-blog
I am building a react app to learn react. I have implemented context to store information about the user when they login and also am making a copy in the localstorage. I verify that users are logged in like this in the App component
function App() {
const [user, setUser] = useContext(UserContext);
return (
<div className="App">
<Router>
<NavBar />
<Switch>
<Route path="/login" exact>
{!user.loggedIn ? <LoginForm /> : <Redirect to="/profile" />}
</Route>
</Switch>
</Router>
</div>
);
And this works fairly well. But if the token has expired; I want the user to be forced into the login page. Which means I need to check the expiration time on every route request as users can be in there profile and if they click on another link they will be redirected to login component. How should I approach this?
I have tried to to use useEffect in app component that sets the state loggedIn to true true when it meets the condition and made a comparison like
useEffect(function () {
const ct = parseInt(Date.now().toString(), 10);
const iat = parseInt(localStorage.getItem('iat'), 10);
if (ct - iat < 60000) {
setUser({ loggedIn: true });
} else {
setUser({ loggedIn: false });
}
});
From my understanding this should happen everytime App component is rendered hence I provide no input to useEffect but this obviously throws a maximum update depth reached error.
How should I implement this behaviour?
Edit:
App component won't rerender because it's never changing and only the subcomponents are. What you can do is, you can put history.location as your dependency in the useEffect, which will trigger the useEffect whenever the path changes. See: How to listen to route changes in react router v4?
If you console.log in your component body, you'll see that it rerenders a lot of times with every node updating. You need to add either an empty dependency array or some dependencies so it only runs the useEffect when it's needed.
useEffect(() => {
//your logic here
}, [])
I think you can also mitigate this error by having another check on your setUser method. This way you'll mitigate a lot of unnecessary calls to the setState method which is one of the reasons for this maximum depth reached error
useEffect(function () {
//other code
if (!loggedIn && ct - iat < 60000) {
setUser({ loggedIn: true });
} else if(loggedIn){
setUser({ loggedIn: false });
}
});
Furthermore, there are usually better ways to check whether the user needs to reauthenticate, if you are using Axios, you can set up a check in the interceptor to do this comparison before making a call and if the user is unauthed, redirect them to login page.
Use useLocation from 'react-router-dom'. Then let location = useLocation() and useEffect(function(){//do stuff}, [location]). This way, we will run the useEffect when the url changes.
I would like to implement auto refresh jwt token before every request to GraphQL with Apollo middleware in React Native app. After every user login he gets two tokens: access and refresh. Access token it is the short one for 30-60 min for using in authorization header. And refresh token it is the long one for 60 days for confirm of refresh token graphql mutation.
My flow:
User login and gets 2 tokens -> put access token to authorization header with Appollo setContext.
User make request to GraphQL -> check expireTime of accessToken on a client side:
-> if it is not expired -> confirm request
-> if it is has expired -> call GraphQL refreshToken mutation -> get new tokens -> confirm request.
For keeping tokens on the client side i use KeyChain storage. Can you tell me please should i use Apollo cache for keeping tokens too? Should i write Apollo state for tokens? And how i can implement my flow?
GraphQL mutation
mutation UpdateTokens($refreshToken: String!, $refreshTokenId: String!)
{
updateTokens(refreshToken: $refreshToken, refreshTokenId: $refreshTokenId) {
user {
name
phone
}
accessToken
refreshToken
}
}
App.js
import React from 'react'
import { ApolloClient } from 'apollo-client'
import { ApolloLink } from 'apollo-link'
import { ApolloProvider } from 'react-apollo'
import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { setContext } from 'apollo-link-context'
import * as Keychain from 'react-native-keychain'
import AppNavigator from './AppNavigator'
const httpLink = createHttpLink({
uri: 'http://localhost:4000'
})
const cache = new InMemoryCache()
const authLink = setContext(async (req, { headers, ...context }) => {
const tokens = await Keychain.getGenericPassword()
const accessToken = tokens.username
return {
headers: {
...headers,
authorization: accessToken ? `Bearer ${accessToken}` : ''
},
...context
}
})
const client = new ApolloClient({
link: ApolloLink.from([authLink, httpLink]),
cache,
connectToDevTools: true
})
const App = () => {
return (
<ApolloProvider client={client}>
<ApolloHooksProvider client={client}>
<AppNavigator />
</ApolloHooksProvider>
</ApolloProvider>
)
}
export default App
I honestly think you are on the right path -- as I read your required functionality I thought of apollo-link-context and was happy to see you were taking that approach. We recently had to implement similar functionality in a react-native app, which entailed attaching custom headers with authentication-related data. To retrieve this data we were required to make an async request, although ours was over the network to a third-party service. We did this all in setContext from the client just as you are. This worked well.
I do not think you need to concern yourself with updating your Apollo cache with your tokens, manually or otherwise, for at least a few reasons. First, given the quasi-sensitive nature of what you are storing, it would be best practice to defer to the more secure storage solution, which in this case is likely the Keychain. In addition, having a single source of truth for the tokens in your application can keep things clean.
Assuming you do not want to write this data to cache, I would double-check Apollo client is not automatically doing so. For example, if you for some reason previously queried your token data, Apollo might automatically update your cache upon receiving the mutation payload. Just something to be mindful of.
Let's say I have a user's account information stored in localStorage (client side). I need my Next.JS app to render the webpage's navbar based on what's stored in localStorage (login or logout button). How can I first obtain the value from the client and then render the page? Or perhaps that isn't even what Next.JS is meant to do?
You can do something like this:
Use a variable in the state to prevent the page from being rendered
Use componentDidMount to load data from localStorage
When data is loaded, setState to allow component to be rendered.
It's a react issue, not a next.js issue.
You could use Conditional rendering for step 1.
Also read up on state here, and lastly componentDidMount.
Update:
Nowadays, I would opt for a React hooks implementation instead, but the idea still stands. useEffect can largely accomplish this with some nuances in some situations.
I also realize that there are some possible caveats with NextJS and SSR logic specifically, so this response may not be sufficient. In such cases, I would also look into some other responses below.
As mentioned at https://stackoverflow.com/a/54819843/895245 I haven't been able to truly get localStorage before the first render, only show a fallback page until that happens.
The fundamental issue is that Next.js maps one URL to one pre-render. And React hydration requires the initial server HTML to match the JavaScript structure:
React expects that the rendered content is identical between the server and the client. It can patch up differences in text content, but you should treat mismatches as bugs and fix them. In development mode, React warns about mismatches during hydration. There are no guarantees that attribute differences will be patched up in case of mismatches. This is important for performance reasons because in most apps, mismatches are rare, and so validating all markup would be prohibitively expensive.
That quote is not very clear if text-only changes work or not but the minimal test below shows that it raises a warning in that case, so you don't want to use it.
Therefore the only sure-fire way it to use useEffect to update the page afterwards.
However, when I've tested, the correct render with localStorage shows up so quickly that the intermediate one it is not noticeable at all, I'm not sure it even happens. The only problem is if you make different API calls on each case, see section "Differentiate between "not logged in" and "haven't decided yet" to avoid doing extra API calls" below for an example of that.
What I would like to do is to give a slightly more concrete idea about what was mentioned in that answer.
SWR example
Here is a complete runnable example where the navbar shows login status: https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app That repository is a fork of this one, both of which are Next.js implementations of the awesome realworld project.
The fallback in that case is just the signed out view of the blog pages, which already contain the key information users are likely to want to see, and can be cached e.g. with ISR.
That demo uses SWR to make the code slightly simpler. The key parts are:
navbar code
login code
The key parts of the code there are:
navbar:
import useSWR from "swr";
const Navbar = () => {
const { data: currentUser } = useSWR("user", key => {
const value = localStorage.getItem(key);
return !!value ? JSON.parse(value) : undefined;
});
login:
import { mutate } from "swr";
const LoginForm = () => {
const handleSubmit = async (e) => {
// Get `user` data structure from API.
mutate("user", data?.user);
We see that when the user logins, we call mutate on the "user" global identifier.
This redraws all components that contain that hook, which includes the navbar, as it setup the hook with the useSWR call.
This way, login first redraws the navbar, and then redirects you to home, so that the home page will have the redrawn navbar immediately. Without mutate, only the page body would redraw, not the navbar.
With this setup:
if you put a console.log(currentUser) just below useSWR, you see that it gets called twice.
So what happens is that it first returns immediately with a cached value (undefined) and the first render starts.
It then starts an async call to the cache, and when that returns, the hook triggers a re-render of the component, and the print happens again with the current user value.
This only happens on initial hydration during refresh/first hit. During internal page changes, there is just a single render.
All of this happens so fast that I can't see the page draw without hte local storage at all, not even with the Chromium debugger timeline frame inspection.
if we add a 2 second delay to the localStorage getter however:
const { data: currentUser } = useSWR("user", async (key) => {
await new Promise(resolve => setTimeout(resolve, 2000))
const value = localStorage.getItem(key);
return !!value ? JSON.parse(value) : undefined;
});
we do observe an intermediate page state with the user logged out, so it could in theory happen.
How it would look like without SWR
Of course, we wouldn't need to use SWR to achieve this.
The SWR documentation gives us the rationale of how thing would look like without SWR https://swr.vercel.app/getting-started to motivate their library.
You would either need to move the state up to a common parent of the login form + navbar, or you could use Context.
function Page () {
const [user, setUser] = useState(null)
// fetch data
useEffect(() => {
const value = localStorage.getItem(key);
const user = !!value ? JSON.parse(value) : undefined;
setUser(user)
}, [])
// global loading state
if (!user) return <Spinner/>
return <div>
<Navbar user={user} />
<Content user={user} />
</div>
}
// child components
function Navbar ({ user }) {
return <div>
...
<Avatar user={user} />
</div>
}
function Content ({ user }) {
return <h1>Welcome back, {user.name}</h1>
}
function Avatar ({ user }) {
return <img src={user.avatar} alt={user.name} />
}
As mentioned at What is difference between lifecycle method and useEffect hook? useEffect is the hook analogue to componentDidMount.
Checking typeof localStorage === 'undefined' leads to a warning
React doesn't like that and warns with something like:
Expected server HTML to contain a matching"
as it notices the difference between hydrated and non-hydrated pages: React 16: Warning: Expected server HTML to contain a matching <div> in <body>
Tested on Next.js 10.2.2.
Minimal reproducible example
Just to play with and see exactly what happens:
pages/index.js
import Link from 'next/link'
import React from 'react'
export default function IndexPage() {
console.error('IndexPage');
let [n, setN] = React.useState(0)
if (typeof localStorage === 'undefined') {
n = '0'
} else {
n = parseInt(localStorage.getItem('n') || '0', 10)
}
return <>
<Link href="/notindex">notindex</Link>
<div
onClick={() => {
localStorage.setItem('n', n + 1)
setN(n + 1)
}}
>increment</div>
<div
onClick={() => {
localStorage.removeItem('n')
setN(0)
}}
>reset</div>
<div>{n}</div>
</>
}
pages/notindex.js
import Link from 'next/link'
export default function NotIndexPage() {
return <Link href="/">index</Link>
}
package.json
{
"name": "test",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "12.0.7",
"react": "17.0.2",
"react-dom": "17.0.2"
}
}
Run:
npm install
npm run dev
Now, if you:
open /
increment
refresh the page
react gives a warning because it notices that the 0 text was changed to 1:
Warning: Text content did not match. Server: "0" Client: "1"
If we click the internal links however to notindex and back, we don't see the warning. This is because hydration is only done on the initial page refresh, further changes are done in Js only.
What we have to do instead is something like this:
import Link from 'next/link'
import React from 'react'
export default function IndexPage() {
console.error('IndexPage');
let [n, setN] = React.useState(0)
React.useEffect(() => {
console.error('useEffect');
setN(parseInt(localStorage.getItem('n') || '0', 10))
}, [])
return <>
<Link href="/notindex">notindex</Link>
<div
onClick={() => {
setN(n + 1)
localStorage.setItem('n', n + 1)
}}
>increment</div>
<div
onClick={() => {
localStorage.removeItem('n')
setN(0)
}}
>reset</div>
<div>{n}</div>
</>
}
Differentiate between "not logged in" and "haven't decided yet" to avoid doing extra API calls
OK, I had another issue: I was making unnecessary API calls, because first the page thought the user was logged out, and then it thought it was logged in, and each of those needed to do different API calls.
Unlike starting to render the wrong page, this would actually have server load consequences, so it was not acceptable.
The solution I used was to differentiate between:
undefined: haven't decided
null: not logged-in
and not make any requests on undefined.
Here's a non-minimized demo:
https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app/blob/2bbce5199d3a7efa19a3a58426bea25a1cd37579/front/ArticleList.tsx#L33
https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app/blob/2bbce5199d3a7efa19a3a58426bea25a1cd37579/front/useLoggedInUser.ts
I'll try to minimize it later on.
Another solution: just do SSR
In general, SSR is way simpler than ISR, because you don't have to worry about this get page/ask for data/get data/update page dance from Hell.
ISR is an optimization, and you should only use if there's a proven performance benefit.
Remember that SSR in Next.js is also very data efficient, as Next.js returns only the .json from getServerSideProps on page switches, basically exactly like an API would.
You can then just do authentication from getServerSideProps with cookies, and return the correct page straightaway.
This is how I did it.
const setSession = (accessToken) => {
if (typeof window !== 'undefined')
localStorage.setItem('accessToken', accessToken);
};
const getAccessToken = () => {
if (typeof window !== 'undefined')
return localStorage.getItem('accessToken');
};
Here is where I call them to handle login and to get the access token:
const loginWithEmailAndPassword = async (email, password) => {
const { data } = await axios.post(`${apiUrl}/login`, { email, password });
const { user, accessToken } = data;
if (user) {
setSession(accessToken);
return user;
}
};
const accessToken = getAccessToken();
local storage is not available on the server, there are two options to resolve this
1: create HOC or custom hook to check if the local storage has the data (this is normal react way)
2: you can use cookies to store data on client and server side , which can be then be used getServerSideProps to extract the data and and you can then use this data to display the information accordingly on the initial render.
you can use useEffect hook and useState, so that when component loads, useEffect will fire last, extract data from localStorage and assign it to a STATE from useState.
then you can access your data from useState, states. if that makes sense.
Bottom line, useEffect allows to easily extract data from localStorage, so then you can do what you like with it.
const [userData, setUserData] = useState({});
console.log(userData);
useEffect(()=> {
setUserData(localStorage.getItem('userSession'));
}, [])
The first render which happen on server side can not have access to localStorage and throw the error. To prevent this, add an extra layer of defense with
if (typeof window !== 'undefined') {
// run logic that read/write localStorage
}
Then it should skip the logic when on server and run it when loaded on client side
I have a React/Redux/React Router front end, Node/Express back end. I’m using Passport (various strategies including Facebook, Google and Github) for authentication.
What I want to happen:
Unauthenticated user attempts to access protected client route
(something like /posts/:postid, and is redirected to /login.
(React Router is handling this part)
User clicks the ‘Log in with Facebook’ button (or other Social auth service)
After authentication, user is automatically redirected back to the route they were attempting to access in step 1.
What is happening instead:
The only way I’ve found to successfully handle Passport social authentication with a React front end is to wrap the ‘Log in with Facebook’ button in an <a> tag:
Facebook Login
If I try to do it as an API call instead of a link I always get an error message (this issue is explained in a lot more detail here: Authentication with Passport + Facebook + Express + create-react-app + React-Router + proxy)
So the user clicks the link, which hits the Express API, successfully authenticates with Passport, and then Passport redirects to the callback route (http://localhost:8080/auth/facebook/callback).
In the callback function I need to (1) return the user object and token to the client, and (2) redirect to a client route — either the protected route they were trying to access before they got redirected to /login, or some default route like / or /dashboard.
But since there isn’t a way to do both of these things in Express (I can’t res.send AND res.redirect, I have to choose one), I’ve been handling it in what feels like kind of a clunky way:
res.redirect(`${CLIENT_URL}/user/${userId}`)
This loads the /user route on the client, and then I’m pulling the userId out of the route params, saving it to Redux, then making ANOTHER call to the server to return the token to save token to localStorage.
This is all working, although it feels clunky, but I can’t figure out how to redirect to the protected route the user was trying to access before being prompted to log in.
I first tried saving the attempted route to Redux when the user tries to access it, thinking I could use that to redirect once they land on the profile page after authentication. But since the Passport auth flow takes the user off-site for 3d-party authentication and then reloads the SPA on res.redirect, the store is destroyed and the redirect path is lost.
What I ended up settling on is saving the attempted route to localStorage, checking to see if there is a redirectUrl key in localStorage when the /user component mounts on the front end, redirecting with this.props.history.push(redirectUrl) and then clearing the redirectUrl key from localStorage. This seems like a really dirty workaround and there has got to be a better way to do this. Has anybody else figuree out how to make this work?
In case anybody else is struggling with this, this is what I ended up going with:
1. When user tries to access protected route, redirect to /login with React-Router.
First define a <PrivateRoute> component:
// App.jsx
const PrivateRoute = ({ component: Component, loggedIn, ...rest }) => {
return (
<Route
{...rest}
render={props =>
loggedIn === true ? (
<Component {...rest} {...props} />
) : (
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
)
}
/>
);
};
Then pass the loggedIn property to the route:
// App.jsx
<PrivateRoute
loggedIn={this.props.appState.loggedIn}
path="/poll/:id"
component={ViewPoll}
/>
2. In /login component, save previous route to localStorage so I can later redirect back there after authentication:
// Login.jsx
componentDidMount() {
const { from } = this.props.location.state || { from: { pathname: "/" } };
const pathname = from.pathname;
window.localStorage.setItem("redirectUrl", pathname);
}
3. In SocialAuth callback, redirect to profile page on client, adding userId and token as route params
// auth.ctrl.js
exports.socialAuthCallback = (req, res) => {
if (req.user.err) {
res.status(401).json({
success: false,
message: `social auth failed: ${req.user.err}`,
error: req.user.err
})
} else {
if (req.user) {
const user = req.user._doc;
const userInfo = helpers.setUserInfo(user);
const token = helpers.generateToken(userInfo);
return res.redirect(`${CLIENT_URL}/user/${userObj._doc._id}/${token}`);
} else {
return res.redirect('/login');
}
}
};
4. In the Profile component on the client, pull the userId and token
out of the route params, immediately remove them using
window.location.replaceState, and save them to localStorage. Then check for a redirectUrl in localStorage. If it exists, redirect and then clear the value
// Profile.jsx
componentWillMount() {
let userId, token, authCallback;
if (this.props.match.params.id) {
userId = this.props.match.params.id;
token = this.props.match.params.token;
authCallback = true;
// if logged in for first time through social auth,
// need to save userId & token to local storage
window.localStorage.setItem("userId", JSON.stringify(userId));
window.localStorage.setItem("authToken", JSON.stringify(token));
this.props.actions.setLoggedIn();
this.props.actions.setSpinner("hide");
// remove id & token from route params after saving to local storage
window.history.replaceState(null, null, `${window.location.origin}/user`);
} else {
console.log("user id not in route params");
// if userId is not in route params
// look in redux store or local storage
userId =
this.props.profile.user._id ||
JSON.parse(window.localStorage.getItem("userId"));
if (window.localStorage.getItem("authToken")) {
token = window.localStorage.getItem("authToken");
} else {
token = this.props.appState.authToken;
}
}
// retrieve user profile & save to app state
this.props.api.getProfile(token, userId).then(result => {
if (result.type === "GET_PROFILE_SUCCESS") {
this.props.actions.setLoggedIn();
if (authCallback) {
// if landing on profile page after social auth callback,
// check for redirect url in local storage
const redirect = window.localStorage.getItem("redirectUrl");
if (redirect) {
// redirect to originally requested page and then clear value
// from local storage
this.props.history.push(redirect);
window.localStorage.setItem("redirectUrl", null);
}
}
}
});
}
This blog post was helpful in figuring things out. The #4 (recommended) solution in the linked post is much simpler and would probably work fine in production, but I couldn't get it to work in development where the server and client have different base URLs, because a value set to localStorage by a page rendered at the server URL will not exist in local Storage for the client URL
Depending on your application architecture, I can give you a couple of ideas, but they are all based on the fundamental :
Once you have backend handling authentication, you need to store the state of the user in your backend as well ( via session cookie / JWT )
You can create a cookie-session store for your express app which cookie, you need to configure properly to use both the domains ( the backend domain and the front-end domain ) or use JWT for this.
Let's go with more details
Use React to check the authentication state
You can implement an end-point in express called /api/credentials/check which will return 403 if the user is not authenticated and 200 if is.
In your react app you will have to call this end-point and check if the user is authenticated or not. In case of not authenticated you can redirect to /login in your React front-end.
I use something similar :
class AuthRoute extends React.Component {
render() {
const isAuthenticated = this.props.user;
const props = assign( {}, this.props );
if ( isAuthenticated ) {
return <Route {...props} />;
} else {
return <Redirect to="/login"/>;
}
}
}
And then in your router
<AuthRoute exact path="/users" component={Users} />
<Route exact path="/login" component={Login} />
In my root component I add
componentDidMount() {
store.dispatch( CredentialsActions.check() );
}
Where CredentialsActions.check is just a call that populates props.user in case we return 200 from /credentials/check.
Use express to render your React app and dehydrate the user state inside the react app
This one is a bit tricky. And it has the presumption that your react app is served from your express app and not as static .html file.
In this case you can add a special <script>const state = { authenticated: true }</script> which will be served by express if the user was authenticated.
By doing this you can do:
const isAuthenticated = window.authenticated;
This is not the best practice, but it's the idea of hydrate and rehydration of your state.
References :
Hydration / rehydration in Redux
Hydrate / rehydrate idea
Example of React / Passport authentication
Example of cookie / Passport authentication