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

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.

Related

Nextjs urql auth exchange running on server when it should run on client

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.

how to handle expiring access token from partner api

I'm new to Next js. I'm sure this is a common issue but I don't know what to search for. Here's an outline:
One of my partners has an API with Bearer auth. The Bearer token comes from an endpoint I call (/auth) with my username and password. That endpoint returns the Bearer token that I use for all other endpoints, but it expires in one day.
How would I handle making API calls on Next.js API routes to this partner? I.e. where would I store this access token so each API route doesn't need to constantly fetch it. And, how do I update it when it expires?
Your clients (once authenticated) should be the ones "storing" these tokens. You would basically need to fetch it from the client's session, cookie, or however you are storing those.
As far as updating these tokens, your auth provider should also provide a "refresh token" that can be used to retrieve a new jwt once it has expired. The purpose here is that you'll be able to refresh the token for the user without requiring them to log in again.
Depending on your provider, this may be a new endpoint you'll need to call.
https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/
If you are making calls to your partner's api when you go to a specific route(like a protected one), then you should store your access token as cookie with http-only flag to avoid security issues like xss attacks that can steal the session data from your browser(the http-only flag should be set in the /auth route in your partner's api when he sends the response with the token), so make the calls within the getServerSideProps sending the cookie with the request, so your partner should take the token from the cookie and validate it to allow the request, one thing i need to point out is that you can't access an http-only cookie from client side, but as you are using nextJs you can still access it from getServerSideProps with a library called nookies, so you could do something like this:
export const getServerSideProps: GetServerSideProps = async ctx => {
const cookies = nookies.get(ctx)
const someApiData = await fetchApiData(cookies)
if (!someApiData) {
return {
redirect: {
// Redirect to home if not authorized
destination: '/',
permanent: false
}
}
}
return {
//return data fetched from the api
props: {
someApiData
}
}
}
and the function that makes the api call, could look like this(note that i'm using axios):
const fetchApiData = async (cookies: CookieData) => {
try {
const result = await axios.get<ApiData>('/some/api/route', {
// Pay attention to this line, here we are sending the cookie with the access token
headers: {
Cookie: `token=${cookies.token}; HttpOnly;`
}
})
return result.data
} catch (error) {
console.log(error)
}
}
Note that you should send the cookie whenever you make a request to a protected route and your partner should validate this token in each route that he wants to protect.
And to refresh the token without login again, your partner can implement a refresh token like #SLee mentioned. This is just an example but you got the idea.

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.

User redirect and authentication with middleware of Nuxt

I'm trying to redirect a user to a login page if the user is not logged in when he tries to access certain pages with the following code.
// middlware/authenticated.js
import firebase from 'firebase'
export default function ({ store, redirect }) {
let user = firebase.auth().currentUser
store.state.user = user //this doesn't work
if (!user) {
console.log('redirect')
return redirect('/login')
}
}
However, the problem is with this code when I refresh a page I'm redirected to login page although without using the middleware, I can stay in the same page with logged in. For some reasons, which I don't know why, firebase can't work in middleware.
How should I modify this middleware or implement this function?
Thanks.
//middleware/authenticated.js
export default function ({
store,
redirect
}) {
if (!store.getters['index/isAuthenticated']) {
return redirect('/login')
}
}
//post.vue
async mounted () {
if (process.browser) {
let user;
if (!this.user) user = await auth(); // this auth is a plugin
await Promise.all([
this.user ? Promise.resolve() : this.$store.dispatch("setUser", { user: user || null })
]);
this.isLoaded = true;
}
},
//plugins/auth.js
import firebase from '~/plugins/firebase'
function auth () {
return new Promise((resolve, reject) => {
firebase.auth().onAuthStateChanged((user) => {
resolve(user || false)
})
})
}
export default auth
By default Firebase persists the users logged in status on successful authentication. This example uses the session, to store the user uid and cookies to store the users token and used in situations where the sessions has ended (example when browser is closed) and then a new session started but where the user is still authenticated according to Firebase. In cases like these the user will not need to sign in to view protected resources.
Your basic Middleware to protect it should look like this (if you have a Store Module called User)
export default function ({ store, redirect }) {
if (!store.getters['modules/user/isAuthenticated']) {
return redirect('/auth/signin')
}
}
In your main Store you use the ServerInit Function to get the User if there is one saved in the Cookies and load it into your User Store Module which will be used for verification in the Middleware.
Your User Store Module should look like this, and keep in mind that you remove the Cookie when you Log the User out so that he is fully logged out.
I used the things i mentioned above as the beginning of my Authentication and modified it a bit, which you can also do. Most of the credit goes to davidroyer who has set up this nice Github Repo which includes all needed files as a good example on how to accomplish your goal.

Resources