supabase / auth-helpers

A collection of framework specific Auth utilities for working with Supabase.
https://supabase.github.io/auth-helpers/
MIT License
903 stars 237 forks source link

Middleware Auth and Logins with redirectTo do not Jive Together #391

Closed quick007 closed 1 year ago

quick007 commented 1 year ago

Bug report

Describe the bug

If you have middleware auth set up (for example, I want all unauthenticated users to go to the login page), when users log in they'll still be taken to the login page despite a redirectTo option set. I'm assuming it can't grab their session for some reason.

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

  1. Create a middleware auth file that redirects to the login page for unauthenticated users:

    
    export async function middleware(req: NextRequest) {
    const res = NextResponse.next();
    const supabase = createMiddlewareSupabaseClient({ req, res });
    const {
    data: { session }
    } = await supabase.auth.getSession();
    
    if (session) {
    return res;
    }
    
    const redirectUrl = req.nextUrl.clone();
    redirectUrl.pathname = '/login';
    redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname);
    return NextResponse.redirect(redirectUrl);
    }

export const config = { matcher: "/((?!api|_next/static|favicon.ico|login|svg|brand-logos).*)", };

2. Create a login page with a `redirectTo`
```tsx
import { useSupabaseClient, useUser } from "@supabase/auth-helpers-react";
import { useRouter } from "next/router";
import { Database } from "../lib/db/database.types";

export default function Login() {
    const supabaseClient = useSupabaseClient<Database>();
    const user = useUser();
    const router = useRouter();

    return (
        <button
            className="mb-8 flex rounded-md bg-gray-200 px-4 py-3 font-medium hover:bg-gray-300"
            onClick={() =>
                supabaseClient.auth.signInWithOAuth({
                    provider: "google",
                    options: {
                        redirectTo:
                            "http://localhost:3000/settings",
                    },
                })
            }
        >
            Continue with Google
        </button>
    );
}
  1. Log in to your account on /login, after logging in, note that it goes back to /login rather than /settings

Expected behavior

A clear and concise description of what you expected to happen.

I expected it to redirect to /settings

Screenshots

If applicable, add screenshots to help explain your problem.

none

System information

Additional context

You can temporarily solve this problem by doing something like this:

if (user) {
    if (typeof redirectedFrom == "string")
        router.push(decodeURI(redirectedFrom));
    else router.push("/");
}
david-plugge commented 1 year ago

The oAuth provider redirects you back to http://localhost:3000/settings. The url contains a hash with the required data (http://localhost:3000/settings#token=the-actual-token) to create a client session but the server is not able to read the url hash. It is therefore necessary to redirect to a page that does not require authentication.

You could create another route /callback that does not require authentication (the user is not redirect to the login page). Set redirectTo to that route and add a query param to the desired page like ?to=/settings. If a user lands on /callback check if there is a session on the client side and then redirect to the correct page using the query param.

This is a known drawback and i think the auth team is doing some work to get auth working without relying on client side scripts.

quick007 commented 1 year ago

Thanks for the info. Under "Additional Context," I shared a similar solution. Hopefully the team will be able to get auth working without client side scripts though!

thorwebdev commented 1 year ago

We need to document this. For the time being you can find some helpful resources here https://github.com/supabase/auth-helpers/issues/341#issuecomment-1319502599 and here https://github.com/supabase/auth-helpers/issues/341#issuecomment-1319597309

phillipleblanc commented 1 year ago

This is a known drawback and i think the auth team is doing some work to get auth working without relying on client side scripts.

This is something we are looking for as well. Ideally we can create the session in getServerSideProps and redirect on the server side, instead of loading an unauthenticated page for a second, creating the session in javascript and then redirecting.

silentworks commented 1 year ago

This will be fixed when PKCE is implemented, but for now the workaround is redirecting to a non-auth'd page first and then to the auth'd page after.

quick007 commented 1 year ago

@silentworks What is PKCE? Also, I have a weird bug where even if the user gets redirected to a non-authed page, they need to refresh once to get redirected. I know this probably isn't where to ask for support, but everyone I've asked can't find a fix. Is this a bug? Heres my code:

useEffect(() => {
        setUrl(window.location.origin);
        if (user && router.isReady) {
            if (typeof redirectedFrom == "string")
                router.push(decodeURI(redirectedFrom));
            else router.push("/");
        }
    }, [user, router, redirectedFrom]);
silentworks commented 1 year ago

@quick007 you can have a look at the diagram here to see how you have to do this currently https://github.com/supabase-community/supabase-by-example/tree/main/magic-link and if you click into the nextjs example you can see the code doing this. PKCE will allow for you to get the token on the server rather than having to depend on the client side code.

quick007 commented 1 year ago

Great, ty

ismaeljtl commented 1 year ago

Hi guys! Had the same problem you were facing here and got to solve it in a hacky way: So, we need to keep in mind that anything we do for checking the auth session server side (middleware or even server router) are not going to take effect because the social auth needs to be ran client side. So what I did was to set the redirectTo of the social auth to the login page and in my login page I added the following code:

export const supabaseClientComponent = createClientComponentClient()
useEffect(() => {
    const {
      data: { subscription },
    } = supabaseClientComponent.auth.onAuthStateChange((event) => {
      if (event === "SIGNED_IN") router.replace("/profile")
    })

    return () => subscription.unsubscribe()
  }, [supabaseClientComponent])

What this is doing is to check for client side auth state changes in the login page, so after I've signed in it will catch the session client side and redirect me to the profile page. This is a nice hack I saw somewhere else. Hope it helps!

dvenomb98 commented 1 year ago

@quick007 Hey, i have exact same problem. It redirect user but URL is same and u have to refresh page manually. Did you solve it somehow?

quick007 commented 1 year ago

@dvenomb98 as silent mentioned above, you can do it server side: https://github.com/supabase-community/supabase-by-example/tree/main/magic-link

I'm lazy and don't want to rewrite my auth flow.

The way I "fixed" it (seems to work every time) is this:

useEffect(() => {
        setUrl(window.location.origin);
        if (user && router.isReady) {
            if (typeof redirectedFrom == "string")
                router.push(decodeURI(redirectedFrom));
            else router.push("/");
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [user, router]);

This is probably hackier than the solution mentioned above (although for the one above, you should probably use router.push instead of replace).