auth-helpers
auth-helpers copied to clipboard
Supabase Auth and Next.js SSR: User redirected back to my website after logging in sees the logged out version of the page.
Hi! I am using Next.js and Supabase Auth to build a platform for selling courses.
When the user is logged in and has bought the course, I want to show them the content. When the user is logged out, I want to show them the paywall. It is important to me that course content is server-side rendered.
I am checking whether the user is logged (and whether they own the course) inside getServerSideProps() like so:
import { getUser } from '@supabase/auth-helpers-nextjs'
import prisma from 'prisma/prismaClient'
export async function getServerSideProps({ params, req, res }) {
// Check whether the user owns this course
let ownsCourse = false
const { user } = await getUser({ req, res }) // Access the user object
if (user) {
const profile = await prisma.profile.findUnique({
where: { id: user.id },
include: { courses: true },
})
ownsCourse = profile.courses.some((c) => c.courseId === courseSlug)
}
[...]
}
My problem is that when the user logs in, and is redirected back to my website, they still see the logged out version of the page (with the paywall). They have to reload the page manually to see the signed in version.
I'm guessing that happens because after the user is redirected back to my website, getServerSideProps() sees that they don't have any auth cookies yet, so it renders the signed out version of the page. Then the frontend code realizes that I just logged in, and sets the auth cookies, but at this point the page has already been rendered.
Can you please share some advice? How can I solve this?
You can see my code here.
Have you considered doing something like:
window.location.reload();
to reload the page?
If you're just looking to change the state of the user once the client app has loaded the user asynchronously, try something like what I use in my code:
supabase.auth.onAuthStateChange(async (event, session) => {
if (event === 'SIGNED_IN' && session) {
this._user = session.user;
// SupabaseAuthService.user.next(session.user); // if using observables
} else if (session === null) {
this._user = null;
// SupabaseAuthService.user.next(null); // if using observables
}
this.loadProfile(); // etc.
});
@burggraf You mean adding something like this in my AuthContext?
useEffect(() => {
[...]
supabaseClient.auth.onAuthStateChange(async (event, session) => {
// First render after sign in fix
if (event === 'SIGNED_IN' && session) {
console.log('Signed in for the first time')
window.location.reload()
}
})
}, [helpersUser, error])
(you can see the full code here).
Unfortunately, this doesn't work. This event seems to keep firing again and again, every time the page reloads. So it gets into an infinite loop of reloading the page.
If it fired only the first time the user arrived on the website after being redirected from supabase auth, I think it might've worked (although it still would be far from an ideal solution, I really wish I wouldn't have to resort to workarounds like manually forcing the browser to reload the page one extra time every time the user signs in).
Okay, so infinite redirect loop was happening because after you've authenticated, supabase redirects you back to your website, and passes an access_token as parameter into the url, so the function above kept triggering the SIGNED_IN event because of that.
If I do this instead, redirect loop doesn't happen:
supabaseClient.auth.onAuthStateChange(async (event, session) => {
if (event === 'SIGNED_IN' && session) {
console.log('Signed in for the first time')
// When the user signs in for the first time, supabase redirects them to <url>#access_token=<token>
// I redirect to url without any parameters, forcing it to refresh the page
// now [...slug] can use the cookies to know that I'm authed in, and render the course content instead of the paywall
window.location.href = location.protocol + '//' + location.host + location.pathname;
}
})
But this STILL doesn't work. Because for some reason, even after I reload the page, the cookies still aren't being set, and the server still renders the paywall. I have to reload the page manually again, and then it works.
So even this ugly workaround doesn't work.
Can someone please help me resolve the original issue, correctly? I can't be the only person struggling with this, this problem impacts everyone who needs to use supabase auth, and server-side renders their pages.
Can you take a look at the example project in this repo as I don't remember it having this issue, it might be something to do with your implementation. https://github.com/supabase-community/auth-helpers/tree/main/examples/nextjs
@silentworks Just made my own version of this project, to demonstrate the issue: https://github.com/lumenwrites/supabase-examples-nextjs
All the changes I've made are here: https://github.com/lumenwrites/supabase-examples-nextjs/blob/main/pages/index.tsx
The only difference is that I'm fetching the user on server-side, in getServerSideProps(), at the end of the file:
import { getUser } from '@supabase/auth-helpers-nextjs'
export async function getServerSideProps({ req, res }) {
const { user } = await getUser({ req, res }) // Access the user object
console.log('[getServerSideProps] User:', user)
return { props: { user } }
}
Here's the deployed version of the project on vercel: https://supabase-examples-nextjs.vercel.app/
Steps to see the issue:
- Login with GitHub.
- When it redirects you back to the website, you will still see the auhted-out version of the page.
- Reload the page. Now you will see the authed-in version of the page.
I know what this issue is, we have the same issue in SvelteKit. This is due to the server loading before the client, and the (cookie saving) request to the server from the client happens after the server has loaded. My current workaround for this is to add a loading page before redirecting to the logged-in view or my latest solution is to delay client-side routing after the form has been submitted.
@silentworks
My current workaround for this is to add a loading page before redirecting to the logged-in view
Unfortunately, I don't think it'll work for me, because it's important for me to redirect the user back to the page they came from, not to /.
Currently I'm doing it like this:
const { user, session, error } = await supabaseClient.auth.signIn(
{ email },
{ redirectTo: window.location.href }
)
But if there's an intermediate page, I'd somehow have to pass window.location.href to it, so that it would know how to redirect the user back to the page they came from. Do you know if it's possible to pass that kind of metadata to the page user lands on after being returned from supabase auth?
or my latest solution is to delay client-side routing after the form has been submitted.
Can you please elaborate on that? How does that work?
By the way, building on top of @burggraf suggestion, I've made this work:
supabaseClient.auth.onAuthStateChange(async (event, session) => {
if (event === 'SIGNED_IN' && session) {
setTimeout(() => {
window.location.href = location.protocol + '//' + location.host + location.pathname
}, 400)
}
})
When the user returns from being authenticated, I force refresh the page. Unfortunately it doesn't work without setTimeout() for some reason, I don't understand why. Like for some reason it can't manage to set the cookies fast enough before the page reloads. And I know this can't be the right way to do things. But at least thats working for now.
I'm having the same issue with my app Notion Maps (https://app.notionmaps.com), I have one login page and the index page with SSR which uses the withPageAuth helper. After the user authenticates, is redirected to the index page / with the token, but then it's redirected again to the login page, and if I manually go to the index page the session is working. I can't reproduce this on local, I don't know why, I'm using a local Supabase instance so maybe is too fast, or maybe is related to some cookie issue.
The state of the cookies is strange too, after the user is redirected from Google, the browser has 5 cookies, 2 from Supabase and 3 from my app, but after I go to the index page, the ones related to Supabase disappear. I don't know if this is related to the issue.
The trick that I'm using right now is to wait a few seconds and check the auth status in the login page and redirect again, which is pretty ugly.
same issue here will this be fixed?
Any update on this?
Any update on this?
Hey @Anan7Codes -- I managed to resolve my particular issue with the resolution of this issue https://github.com/supabase/supabase/issues/4540
This issue has perplexed me for a while. My happy path:
- user signs up via
/signuproute - user receives a Confirmation email
- user clicks link in Confirmation email. This verifies the email and redirects them to
/dashboardroute - user arrives at
/dashboardroute and is signed in
The last step is currently not possible if the /dashboard route is server-rendered. In a NextJS app using @supabase/auth-helpers, that comes in the form of withPageAuth:
// pages/dashboard.js
const Dashboard = () => {/* page stuff */}
export const getServerSideProps = withPageAuth({ redirectTo: '/login' })
This is a simplification of what I'm doing on the /dashboard route. What's currently happening is:
- user get redirected to the
/loginroute - user manually navigates to the
/dashboardroute, and they're logged in (ymmv depending on redirect logic)
There are a few ways around this, (intermediate page, onAuthStateChange, etc.) but it would be nice if this worked as expected.
This issue has perplexed me for a while. My happy path:
- user signs up via
/signuproute- user receives a Confirmation email
- user clicks link in Confirmation email. This verifies the email and redirects them to
/dashboardroute- user arrives at
/dashboardroute and is signed inThe last step is currently not possible if the
/dashboardroute is server-rendered. In a NextJS app using@supabase/auth-helpers, that comes in the form ofwithPageAuth:// pages/dashboard.js const Dashboard = () => {/* page stuff */} export const getServerSideProps = withPageAuth({ redirectTo: '/login' })This is a simplification of what I'm doing on the
/dashboardroute. What's currently happening is:
- user get redirected to the
/loginroute- user manually navigates to the
/dashboardroute, and they're logged in (ymmv depending on redirect logic)There are a few ways around this, (intermediate page,
onAuthStateChange, etc.) but it would be nice if this worked as expected.
Can you show an example with an intermediate page, and another example with onAuthStateChange? That would be very useful for me :)
As it stands today, you will need to set the redirect URL to be a client-side route, e.g. your sign-in page, and then have a hook that monitors the user object and redirects to the desired page, e.g. see this example: https://github.com/vercel/nextjs-subscription-payments/blob/main/pages/signin.tsx#L58-L62
const { user } = useUser();
useEffect(() => {
if (user) {
router.replace('/account');
}
}, [user]);
That's because the sign-in state is currently communicated via a URL fragment and therefore can't be accessed server-side.