Nextjs urql auth exchange running on server when it should run on client - next.js

When trying to add an auth exchange to my urql client, it gets run on the server when the app starts and on the client subsequent times until refresh. The problem is in my getAuth function, which is as follows:
const getAuth = async ({ authState }) => {
const token = localStorage.getItem('5etoken');
if (!authState) {
if (token) {
return { token };
}
return null;
}
if (token) {
const decoded = jwt.decode(token) as jwt.JwtPayload;
if (decoded.exp !== undefined && decoded.exp < Date.now() / 1000) {
return { token };
}
}
return null;
};
When I run my app, I get an error saying localStorage is undefined. If I check that the function is running in the browser, then my token never gets set on app start and I'm logged out when I refresh the page, so I can't use that approach. I've tried multiple approaches:
Using dynamic imports with ssr set to false
Creating the client in a useEffect hook
Using next-urql's withUrqlClient HOC only using the auth exchange when in the browser
None of what I tried worked and I'm running out of ideas.

I eventually figured out that createClient was being called on the server side. I managed to force it to run in the browser by creating the client in a useEffect hook. I'm not sure why creating it in a useEffect didn't work months ago.

Related

How to use secret key for on-demand revalidation for ISR (Next.js) in the frontend without exposing it?

According to the documentation, you should use a SECRET_TOKEN to prevent unauthorized access to your revalidate API route i.e.
https://<your-site.com>/api/revalidate?secret=<token>
But how are you supposed to call that route from the frontend and keep the token secret?
For example, if you have a simple POST that you then want to trigger the revalidate off of, you'd have to expose your secret token via NEXT_PUBLIC to be able to use it:
function handleSubmit(payload) {
axios.post(POST_URL, payload)
.then(() => {
axios.get(`/api/revalidate?secret=${process.env.NEXT_PUBLIC_SECRET_TOKEN}`)
})
.then(() => {
// redirect to on-demand revalidated page
})
}
What am I missing here? How can you call the API route through the frontend without exposing the SECRET_TOKEN?
I've been trying out On-Demand ISR and stumbled on a similar problem. I was trying to revalidate data after CRUD actions from my Admin dashboard living on the client, behind protected routes ("/admin/...").
If you have an authentication process setup and you're using Next-Auth's JWT strategy, it gives you access to the getToken() method, which decrypts the JWT of the current authenticated user.
You can then use whatever information you have passed through your callbacks to validate the request instead of relying on a SECRET_TOKEN.
import type { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
const secret = process.env.NEXTAUTH_SECRET;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const user = await getToken({ req, secret });
if (!user || user.role !== "ADMIN") {
return res.status(401).json({ message: "Revalidation not authorized"});
}
try {
// unstable_revalidate is being used in Next 12.1
// I'm passing the revalidation url through the query params
await res.unstable_revalidate(req.query.url as string);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send("Error revalidating");
}
}
The Next.js video demo don't actually use a SECRET_KEY.
https://www.youtube.com/watch?v=BGexHR1tuOA
So I guess I'll just have to omit it and hope nobody abuses the revalidate API?
I think you need to create one file called ".env".
Inside the file, you put the params .env like this:
NEXT_PUBLIC_SECRET_TOKEN=123password
You must install the dependency dotenv:
npm i dotenv
and then you can call inside your function like this
function handleSubmit(payload) {
axios.post(POST_URL, payload)
.then(() => {
axios.get(`/api/revalidate?secret=${process.env.NEXT_PUBLIC_SECRET_TOKEN}`)
})
.then(() => {
// redirect to on-demand revalidated page
})
}

Next.js does not render authentication state properly after keycloak login

I have a basic next.js application that does two things:
provide an authentication mechanism using keycloak
talk to a backend server that authorizes each request using the keycloak-access-token
I use the #react-keycloak/ssr library to achieve this. The problem now is that after I login and get redirected back to my application the cookie that contains the kcToken is empty. After I refresh my page it works like expected.
I understand that maybe my entire process flow is wrong. If so, what is the "usual" way to achieve what is mentioned above?
export async function getServerSideProps(context) {
const base64KcToken = context.req.cookies.kcToken // the cookie that keycloak places after login
const kcToken = base64KcToken ? Buffer.from(base64KcToken, "base64") : ""
// the backend server passes the token along to keycloak for role-based authorization
const res = await fetch(`${BACKEND_URL}/info`, {
headers: {
"Authorization": "Bearer " + kcToken
}
})
const data = await res.json()
// ... exception handling is left out for readability ...
return {
props: {
data
}
}
}
export default function Home({data}) {
const router = useRouter() // the next.js client side router to redirect to keycloak
const { keycloak, initialized } = useKeycloak() // keycloak instance configured in _app.js
if (keycloak && !initialized && keycloak.createLoginUrl) router.push(keycloak.createLoginUrl())
return (
<div> ... some jsx that displays data ... </div>
)
}
This process basically works but it feels really bad because a user that gets redirected after login is not able to see the fetched data unless he refreshes the entire page. This is because when getServerSideProps() is called right after redirect the base64KcToken is not there yet.
Also everything related to the login-status (eg. logout button) only gets displayed after ~1sec, when the cookie is loaded by the react-keycloak library.

Issues with NextJs SSR and cookies in Apollo

I would like to use cookies for authentication in my nextjs app. I have a bug in my code where the SSR won't work because somewhere in the execution process of the code it does not find the cookie on the first render of the page so it will throw an error. I have played with the code a lot now and have gotten it to a state where the data will eventually load but will not be a SSR page. Has anyone else dealt with this problem?
I am using next, apollo client and apollo server express.
When you do an SSR, the code runs on the server. The cookies you added in browser are not available as default. You can access then in getInitialProps or getServerSideProps via req.headers.cookie and pass it to the authentication code again.
Alternately, you can use an npm module like react-cookie https://www.npmjs.com/package/react-cookie which support isomorphic cookies. More examples on integration are available on the link.
We can custom the headers before sending.
Please check my full answer at this link https://github.com/apollographql/apollo-client/issues/5089#issuecomment-749301669
async function getHeaders(ctx) {
if (ctx?.req?.cookies) {
const cookieItems = []
for (let key of Object.keys(ctx?.req?.cookies)) {
cookieItems.push(`${key}=${ctx.req.cookies[key]}`)
}
return {
cookie: cookieItems.join('; ')
}
}
return {
}
}
WithApollo.getInitialProps = async (ctx) => {
const { AppTree } = ctx
// Initialize ApolloClient, add it to the ctx object so
// we can use it in `PageComponent.getInitialProp`.
const apolloClient = (ctx.apolloClient = initApolloClient(null, await getHeaders(ctx)))
// Run wrapped getInitialProps methods
let pageProps = {}
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx)
}
............
}
}

NuxtJS state changes and firebase authentication

I am still a nuxt beginner, so please excuse any faults.
I am using the "official" firebase module for nuxt https://firebase.nuxtjs.org/ to access firebase services such as auth signIn and singOut.
This works.
However, I am using nuxt in universal mode and I cannot access this inside my page fetch function. So my solution is to save this info in the vuex store and update it as it changes.
So, once a user is logged in or the firebase auth state changes, a state change needs to happen in the vuex store.
Currently, when a user logs in or the firebase auth state changes, if the user is still logged in, I save the state to my store like so :
const actions = {
async onAuthStateChangedAction(state, { authUser, claims }) {
if (!authUser) {
// claims = null
// TODO: perform logout operations
} else {
// Do something with the authUser and the claims object...
const { uid, email } = authUser
const token = await authUser.getIdToken()
commit('SET_USER', { uid, email, token })
}
}
}
I also have a mutation where the state is set, a getter to get the state and the actual state object as well to store the initial state:
const mutations = {
SET_USER(state, user) {
state.user = user
}
}
const state = () => ({
user: null
})
const getters = {
getUser(state) {
return state.user
}
}
My problem is, on many of my pages, I use the fetch method to fetch data from an API and then I store this data in my vuex store.
This fetch method uses axios to make the api call, like so:
async fetch({ store }) {
const token = store.getters['getUser'] //This is null for a few seconds
const tempData = await axios
.post(
my_api_url,
{
my_post_body
},
{
headers: {
'Content-Type': 'application/json',
Authorization: token
}
}
)
.then((res) => {
return res.data
})
.catch((err) => {
return {
error: err
}
console.log('error', err)
})
store.commit('my_model/setData', tempData)
}
Axios needs my firebase user id token as part of the headers sent to the API for authorization.
When the fetch method runs, the state has not always changed or updated yet, and thus the state of the user is still null until the state has changed, which is usually about a second later, which is a problem for me since I need that token from the store to make my api call.
How can I wait for the store.user state to finish updating / not be null, before making my axios api call inside my fetch method ?
I have considered using cookies to store this information when a user logs in. Then, when inside the fetch method, I can use a cookie to get the token instead of having to wait for the state to change. The problem I have with this approach is that the cookie also needs to wait for a state change before it updates it's token, which means it will use an old token upon the initial page load. I might still opt for this solution, it just feels like it's the wrong way to approach this. Is there any better way to handle this type of conundrum ?
Also, when inside fetch, the first load will be made from the server, so I can grab the token from the cookie, however the next load will be from the client, so how do I retrieve the token then if the store value will still be null while loading ?
EDIT:
I have opted for SPA mode. After thinking long and hard about it, I don't really need the nuxt server and SPA mode has "server-like" behaviour, where you could still use asyncdata and fetch to fetch data before pages render, middleware still works similar and authentication actually works where you dont have to keep the client and server in sync with access tokens etc. I would still like to see a better solution for this in the future, but for now SPA mode works fine.
I came across this question looking for a solution to a similar problem. I had a similar solution in mind as mentioned in the other answer before coming to this question, what I was looking for was the implementation details.
I use nuxt.js, the first approach that came to my mind was make a layout component and render the <Nuxt/> directive only when the user is authenticated, but with that approach, I can have only one layout file, and if I do have more than one layout file I will have to implement the same pre-auth mechanism across every layout, although this is do-able as now I am not implementing it in every page but implementing in every layout which should be considerably less.
I found an even better solution, which was to use middlewares in nuxt, you can return a promise or use async-await with the middleware to stop the application mounting process until that promise is resolved. Here is the sample code:
// middleware/auth.js
export default async function ({ store, redirect, $axios, app }) {
if (!store.state.auth) { // if use is not authenticated
if (!localStorage.getItem("token")) // if token is not set then just redirect the user to login page
return redirect(app.localePath('/login'))
try {
const token = localStorage.getItem("token");
const res = await $axios.$get("/auth/validate", { // you can use your firebase auth mechanism code here
headers: {
'Authorization': `Bearer ${token}`
}
});
store.commit('login', { token, user: res.user }); // now just dispatch a login action to vuex store
}
catch (err) {
store.commit('logout'); // preauth failed, so dispatch logout action which will clear localStorage and our Store
return redirect(app.localePath('/login'))
}
}
}
Now you can use this middleware in your page/layout component, like so:
<template>
...
</template>
<script>
export default {
middleware: "auth",
...
}
</script>
One way of fixing this is to do the firebase login before mounting the app.
Get the token from firebase, save it in vuex and only after that mount the app.
This will ensure that by the time the pages load you have the firebase token saved in the store.
Add checks on the routes for the pages that you don't want to be accessible without login to look in the store for the token (firebase one or another) and redirect to another route if none is present.

Uncaught TypeError: with Redux Thunk and node.js express wwwhisper middleware on MERN stack application

I am building a MERN stack application and trying to use the connect-wwwhisper package to protect access to an application (testing beta version) that I am hosting. I am using passport authentication on the Node js backend for user authentication but I want to layer wwwhisper package on entire app so that only people with approved email may access the entire app without disturbing the passport authentication that I set up. I have set up wwwhisper per the documentation: https://devcenter.heroku.com/articles/wwwhisper but there is a conflict with the redux thunk middleware that is causing a type error within the redux js file below:
function compose() {
for (var _len = arguments.length, funcs = new Array(_len), _key =
0;
_key < _len; _key++) {
funcs[_key] = arguments[_key];
}
if (funcs.length === 0) {
return function (arg) {
return arg;
};
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(function (a, b) {
return function () {
return a(b.apply(void 0, arguments));
The error message is: Uncaught TypeError: Cannot read property 'apply' of undefined
In my server js file I am using the following to direct requests to the index.html file of the react side of the application. All other requests to the backend api are using the
app.use("routename");
if (process.env.NODE_ENV === "production") {
app.use(express.static("client/build"));
app.get("*", (req, res) => {
res.sendFile(path.resolve(__dirname, "client", "build",
"index.html"));
});
}
The wwwhisper middleware does protect the application and sends out the tokenized link to access the application but when I try to access the application I get the above error message along with a message saying the token is unauthorized. The author of the wwwhisper middleware is not familiar with how the wwwhisper middleware may be interacting with the Redux thunk middleware. How can I get this to work? I've been programming for about a year so any help is appreciated.

Resources