supabase icon indicating copy to clipboard operation
supabase copied to clipboard

User only available after middleware has run

Open mwohlan opened this issue 2 years ago • 13 comments

Version

@nuxtjs/supabase: v0.1.9 nuxt: v3.0.0-rc.1

Steps to reproduce

create global auth guard like this:

export default defineNuxtRouteMiddleware((to) => {
  const user = useSupabaseUser()
  if (!(user.value) && to.name !== 'login')
    return navigateTo('/login')
})

create login page with logic like this



const client = useSupabaseClient()

const user = useSupabaseUser()

const email = ref('')

const signIn = async() => {
  const { session, error } = await client.auth.signIn({ email: email.value })

}

You'll get a link like this which redirects you to your page with the credentials to setup a Supabase User.

https://pjbhhvurmrikesghutkr.supabase.co/auth/v1/verify?token=myToken&type=magiclink&redirect_to=http://localhost:3000/

What is Expected?

I should be redirected to the url in the redirect_to query param in the link

What is actually happening?

The user is undefined when the authguard is running its logic and therefore the user is redirected to the login page instead of the original destination. The user only becomes available after the navigation has finished, leaving the user on the login page, even though they are logged in. We could watch the user on the login route and redirect, but this seems unnecessary.

Edit:

Also getting this error on the server side when using the link in the email:

[nuxt] [request error] Auth session missing!
  at ./.nuxt/dev/index.mjs:651:13  
  at processTicksAndRejections (node:internal/process/task_queues:96:5)  
  at async ./node_modules/.pnpm/[email protected]/node_modules/h3/dist/index.mjs:417:19  
  at async Server.nodeHandler (./node_modules/.pnpm/[email protected]/node_modules/h3/dist/index.mjs:367:7)

mwohlan avatar Apr 22 '22 09:04 mwohlan

This seems to be related to supabase/supabase-js#25, I get exactly the same results.

The user seems to be registered too late by the onAuthStateChange handler for the middleware to get the data.

To prevent that I'm setting the user manually in my repo when I get the response of the signIn or signUp. This allows the navigation to work. See here for the temporary implementation I've tried.

This doesn't fix the error message about the session. I'm still trying to get it to work and to get a cleaner implementation.

EDIT: I'm not using magic links but emails confirmations, I get the exact same case with email confirmation links.

ColinEspinas avatar Apr 22 '22 13:04 ColinEspinas

This issue from the supabase repository also seems to be related.

Shouldn't the module already do that for you, what you are doing in your pinia store ? The module already watches the auth events and sets the user, or am I missing something ?

mwohlan avatar Apr 22 '22 13:04 mwohlan

@mwohlan The module listens to the onAuthStateChange function to change the user value. This happens to late for the middleware get the user data. Just as you mentioned, the navigation already has finished when this event is received. So the solution I've come with is setting the user directly from the response of the signIn or signUp method.

I'm not super happy with the way I've done it and I'm searching for another way too.

ColinEspinas avatar Apr 22 '22 14:04 ColinEspinas

I think this is a supabase timing issue. This example call:

 await client.auth.signIn()
 navigateTo('auth protected route')

is done before the user is set here:

  // Once Nuxt app is mounted
  nuxtApp.hooks.hook('app:mounted', () => {
    // Listen to Supabase auth changes
    client.auth.onAuthStateChange(async (event: AuthChangeEvent, session: Session | null) => {
      await setServerSession(event, session)
      user.value = client.auth.user()
    })
  })
})

This explains the need for a workaround in any case (also when logging in using a magic link). It's all doable but feels kinda hacky.

mwohlan avatar Apr 22 '22 16:04 mwohlan

Current workaround for me that works with auth protected routes and magic links:

middleware

export default defineNuxtRouteMiddleware((to) => {
  const user = useSupabaseUser()

  if (!(user.value))
    //reroute to login saving the current destination in the redirect query param
    return navigateTo({ name: 'login', query: { redirect: to.path } })
})

plugin to watch route and user

export default defineNuxtPlugin(() => {
  const user = useSupabaseUser()
// globally watch user and route. If a user and a redirect query param exist: 
  watchEffect(() => {
    if (user.value) {
      const route = useRoute()
      if (route.query.redirect)
        navigateTo({ path: route.query.redirect as string })
    }
  })
})

sign in logic on the login page:

const signIn = async() => {
// the redirectTo param is for magic/confirmation links 
  const { session, error } = await client.auth.signIn({ email: email.value }, { redirectTo: `http://localhost:3000${route.query?.redirect}` })
// this is for username + password sign in to trigger the watchEffect. The component doesnt get rerendered afaik
navigateTo({ name: 'login', query: { redirect: route.query.redirect } })
}

It works for both use cases but is also quite hacky.

mwohlan avatar Apr 22 '22 17:04 mwohlan

Thanks for that, I'll try it too. Does that solves the session error issue too ?

ColinEspinas avatar Apr 22 '22 17:04 ColinEspinas

Thanks for that, I'll try it too. Does that solves the session error issue too ?

@ColinEspinas I commented on that in your original issue #25. Maybe they shouldnt throw an error but simply return in that case ?

mwohlan avatar Apr 22 '22 17:04 mwohlan

@mwohlan Tried it and it works, its unfortunate that we need to use tricks like those but at least it gets the job done.

ColinEspinas avatar Apr 23 '22 22:04 ColinEspinas

I think this is a supabase timing issue. This example call:

 await client.auth.signIn()
 navigateTo('auth protected route')

is done before the user is set here:

  // Once Nuxt app is mounted
  nuxtApp.hooks.hook('app:mounted', () => {
    // Listen to Supabase auth changes
    client.auth.onAuthStateChange(async (event: AuthChangeEvent, session: Session | null) => {
      await setServerSession(event, session)
      user.value = client.auth.user()
    })
  })
})

This explains the need for a workaround in any case (also when logging in using a magic link). It's all doable but feels kinda hacky.

I opened an issue on the supabase-js repository for the client side sync issue

mwohlan avatar Apr 25 '22 08:04 mwohlan

Hello everyone,

I have this problem too. My global auth middleware send me to /login on refresh. This because useSupabaseUser() state is set after middleware has run on page refresh (F5).

I made some experiments with firebase and came to a similar problem wich made me think it is Nuxt 3 related. My supposition is onAuthStateChange functions are watchers, and watcher always run after middleware. But I have to say I am a "noob" dev, so I mightmiss a piece of the puzzle. Should I copy this issue on Nuxt3 github ?

As a temporary fix, I manually register useSupabaseUser.value on signIn() and manually reset it on signOut() but it does not seem the cleanest way.

Zebnastien avatar Jul 25 '22 16:07 Zebnastien

Using setTimeout fixed it for me.

const signIn = async () => {
  await client.auth.signIn({
    email: email.value,
    password: password.value,
  })
  await new Promise((resolve) => {
    setTimeout(resolve, 50)
  })
  console.log(user.value)

  navigateTo('/')
}

IIFelix avatar Aug 06 '22 20:08 IIFelix

Using setTimeout fixed it for me.

const signIn = async () => {
  await client.auth.signIn({
    email: email.value,
    password: password.value,
  })
  await new Promise((resolve) => {
    setTimeout(resolve, 50)
  })
  console.log(user.value)

  navigateTo('/')
}

Doesn't work on my end...

aspect-vs avatar Aug 11 '22 14:08 aspect-vs

@IIFelix has the right idea I think, but instead of waiting for 50ms, wait until the auth handler callback has completed. This works for me. Add data to the component for useSupabaseUser(), and when the callback comletes that data will be updated. Wait to redirect until we see the user session populate. Here's rough example:

export default defaultNuxtComponent({
  setup() {
    return {
      loginUser: useSupabaseUser(),
      supabase: getSupabaseClient(),
    },
  },
  data() {
    return {
      email: '[email protected]',
      password: 'they-might-be-giants',
    },
  }
  methods: {
    async tryLogin() {
      const { user, error } = await this.supabase.auth.signIn({
        email: this.email,
        password: this.password,
      });
      if (error) {
        console.error('oh no, anyway', error)
      } else if (user) {
        const timer = setInterval(() => {
          if (this.loginUser && this.loginUser.id) {
            // Wait for @nuxtjs/supabase auth callback to complete before redirect
            clearInterval(timer)
            await navigateTo({ path: '/hello-friend' });
          }
        }, 100)
      }
    }
  },
});

mitchjacksontech avatar Aug 11 '22 19:08 mitchjacksontech

For anyone who is still having this issue, I was able to resolve it by using a watchEffect:

const user = useSupabaseUser();

async function signin() {
  const { data, error } = await client.auth.signInWithPassword({email, password});
}

watchEffect(async () => {
  if (user.value) await navigateTo("/browse");
});

ymansurozer avatar Dec 15 '22 13:12 ymansurozer