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