Unable to access session data after sign in with OAuth provider (specifically Github) in next js + @supabase/ssr

78 views Asked by At

I am creating a next app that protects some urls based on user authentication. I have an auth form set up like this

import { getUrl } from "@/lib/utils";
import { Button } from "./ui/button";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { formSchema } from "@/schemas";
import { zodResolver } from "@hookform/resolvers/zod";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "./ui/form";
import { Input } from "./ui/input";
import Image from "next/image";
import { Loader2 } from "lucide-react";
import { useState } from "react";
import { ERROR_MESSAGE_TITLE, supabase } from "@/constants";
import { toast } from "./ui/use-toast";

const AuthForm = ({
  onSubmit,
  isLoading,
  btnText,
}: {
  onSubmit: (formData: z.infer<typeof formSchema>) => void;
  isLoading: boolean;
  btnText: string;
}) => {
  const [showPassword, setShowPassword] = useState(false);
  const [isGoogleSignInLoading, setIsGoogleSignInLoading] = useState(false);
  const [isGithubSignInLoading, setIsGithubSignInLoading] = useState(false);

  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      email: "",
      password: "",
    },
  });

  const signInWithGoogle = async () => {
    setIsGoogleSignInLoading(true);

    try {
      const { error } = await supabase.auth.signInWithOAuth({
        provider: "google",
        options: {
          redirectTo: getUrl(),
        },
      });

      if (error) {
        toast({
          title: ERROR_MESSAGE_TITLE,
          description: "An error occurred during login",
          variant: "destructive",
        });
      }
    } catch (error) {
      toast({
        title: ERROR_MESSAGE_TITLE,
        description: "An error occurred during login",
        variant: "destructive",
      });
    }
  };

  const signInWithGithub = async () => {
    setIsGithubSignInLoading(true);

    try {
      const { error } = await supabase.auth.signInWithOAuth({
        provider: "github",
        options: {
          redirectTo: getUrl(),
        },
      });

      if (error) {
        toast({
          title: ERROR_MESSAGE_TITLE,
          description: error.message,
          variant: "destructive",
        });
      }
    } catch (error) {
      toast({
        title: ERROR_MESSAGE_TITLE,
        description: "An error occurred during login",
        variant: "destructive",
      });
    }
  };

  return (
    <>
      <div className="flex justify-between flex-col space-y-4">
        <Button
          className="bg-transparent border text-dark hover:bg-gray-50 text-sm"
          onClick={signInWithGoogle}
          disabled={isGoogleSignInLoading ? true : false}
        >
          {isGoogleSignInLoading && (
            <Loader2 className="mr-2 h-4 w-4 animate-spin" />
          )}
          Continue with Google
        </Button>
        <Button
          className="bg-transparent text-dark border hover:bg-gray-50 text-sm"
          onClick={signInWithGithub}
          disabled={isGithubSignInLoading ? true : false}
        >
          {isGithubSignInLoading && (
            <Loader2 className="mr-2 h-4 w-4 animate-spin" />
          )}
          Continue with GitHub
        </Button>
      </div>
      <div className="w-full flex justify-between space-x-4 mt-4 items-center">
        <span className="h-[0.08px] bg-gray-200 flex-1"></span>
        <p className="text-sm">Or</p>
        <span className="h-[0.08px] bg-gray-200 flex-1"></span>
      </div>
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="w-full">
          <div className="space-y-[1.12rem]">
            <FormField
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem className="mt-[1.12rem]">
                  <FormLabel className="text-sm">Email</FormLabel>
                  <FormControl>
                    <Input type="email" className="outline-none" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem className="mb-0">
                  <FormLabel className="text-sm">Password</FormLabel>
                  <FormControl>
                    <div className="relative">
                      <Input
                        type={showPassword ? "text" : "password"}
                        className="outline-none pr-[2.5rem]"
                        {...field}
                      />
                      <Image
                        src={showPassword ? "/eye-closed.svg" : "/eye.svg"}
                        height={25}
                        width={19}
                        alt="eye.svg"
                        className="absolute cursor-pointer top-[29%] right-3"
                        onClick={() => setShowPassword(!showPassword)}
                      />
                    </div>
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
          </div>

          <Button
            className="w-full mt-[0.75rem] text-sm"
            type="submit"
            disabled={isLoading ? true : false}
          >
            {isLoading ? (
              <>
                <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Please wait...
              </>
            ) : (
              btnText
            )}
          </Button>
        </form>
      </Form>
    </>
  );
};

export default AuthForm;

I also have the following constants defined in constants.ts

import { createBrowserClient } from "@supabase/ssr";

const supabaseUrl = parsedEnv.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = parsedEnv.NEXT_PUBLIC_SUPABASE_ANON_KEY;
export const supabase = createBrowserClient(supabaseUrl, supabaseKey);

this is my auth/callback/route.ts file

import { parsedEnv } from "@/schemas";
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { type NextRequest, NextResponse } from "next/server";
import { isAuthApiError } from "@supabase/supabase-js";

const createCustomClient = (request: NextRequest) => {
  const response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  });

  const supabase = createServerClient(
    parsedEnv.NEXT_PUBLIC_SUPABASE_URL,
    parsedEnv.NEXT_PUBLIC_SUPABASE_ANON_KEY,
    {
      cookies: {
        get(name: string) {
          return request.cookies.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          response.cookies.set({
            name,
            value,
            ...options,
          });
        },
        remove(name: string, options: CookieOptions) {
          response.cookies.set({
            name,
            value: "",
            ...options,
          });
        },
      },
    }
  );
  return { supabase, response };
};

export async function GET(request: NextRequest) {
  const requestUrl = new URL(request.nextUrl);
  const code = requestUrl.searchParams.get("code");

  if (code) {
    try {
      const { supabase } = createCustomClient(request);
      await supabase.auth.exchangeCodeForSession(code);
    } catch (error) {
      if (isAuthApiError(error)) {
        console.log(error);
      } else {
        throw error;
      }
    }
    console.log("success");
    return NextResponse.redirect(requestUrl.origin);
  }
  return NextResponse.redirect(requestUrl.origin);
}

Once the user is authenticated and tries to visit a protected page, the middleware which checks for a session is ran but no session is returned which is not what is expected. On checking, the cookies both exist in the browser cookie storage and it also prints on the console. The issue is that, I need this session to navigate the user to the require page i.e. to login if not authenticated.

This is how the middleware is set up

import { NextResponse, NextRequest } from "next/server";
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { parsedEnv } from "./schemas";

export async function middleware(request: NextRequest) {
  console.log("running middleware");
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  });

  const supabase = createServerClient(
    parsedEnv.NEXT_PUBLIC_SUPABASE_URL,
    parsedEnv.NEXT_PUBLIC_SUPABASE_ANON_KEY,
    {
      cookies: {
        get(name: string) {
          console.log("name of cookie in middleware", name);
          return request.cookies.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value,
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value,
            ...options,
          });
        },
        remove(name: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value: "",
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value: "",
            ...options,
          });
        },
      },
    }
  );

  const { data } = await supabase.auth.getSession();

  console.log("session data", data);

  return response;
}

export const config = {
  matcher: ["/search", "/my-profile", "/meal-planner"],
};

I also included a link to the repository here

0

There are 0 answers