S
Supabase2y ago
dave

Reset Password flow confusion/not working

I have a supabase/ssr + Remix project set up with the following versions:
@remix - 2.2.0 (node, express, react)
"@supabase/ssr": "^0.0.10",
"@supabase/supabase-js": "^2.39.0",
"react": "^18.2.0",
@remix - 2.2.0 (node, express, react)
"@supabase/ssr": "^0.0.10",
"@supabase/supabase-js": "^2.39.0",
"react": "^18.2.0",
I've worked through the sign up and login flows and started to add the reset password flow to my app. I've created singleton objects for the server and browser client, both using @supabase/ssr like so: createServerClient
export function getSupabaseWithHeaders({ request }: { request: Request }) {
const cookies = parse(request.headers.get("Cookie") ?? "");
const headers = new Headers();

const supabase = createServerClient(
process.env.DATABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false,
},
cookies: {
get(key) {
return cookies[key];
},
set(key, value, options) {
headers.append("Set-Cookie", serialize(key, value, options));
},
remove(key, options) {
headers.append("Set-Cookie", serialize(key, "", options));
},
},
},
);

return { supabase, headers };
}
export function getSupabaseWithHeaders({ request }: { request: Request }) {
const cookies = parse(request.headers.get("Cookie") ?? "");
const headers = new Headers();

const supabase = createServerClient(
process.env.DATABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false,
},
cookies: {
get(key) {
return cookies[key];
},
set(key, value, options) {
headers.append("Set-Cookie", serialize(key, value, options));
},
remove(key, options) {
headers.append("Set-Cookie", serialize(key, "", options));
},
},
},
);

return { supabase, headers };
}
createBrowserClient(env.DATABASE_URL!, env.SUPABASE_ANON_KEY!),
createBrowserClient(env.DATABASE_URL!, env.SUPABASE_ANON_KEY!),
The email triggers just fine, I'm able to find it in inBucket and I get both the link and the OTP in the email. The (plaintext) email looks like this:
--------------
Reset password
--------------

Follow this link to reset the password for your user:

Reset password ( http://127.0.0.1:54321/auth/v1/verify?token=pkce_56c6bf5cf091e74770b21ffe167eb6af9b854c44cba8ec84b8b08ea8&type=recovery&redirect_to=http://127.0.0.1:3000/auth/update )

Alternatively, enter the code: 657886
--------------
Reset password
--------------

Follow this link to reset the password for your user:

Reset password ( http://127.0.0.1:54321/auth/v1/verify?token=pkce_56c6bf5cf091e74770b21ffe167eb6af9b854c44cba8ec84b8b08ea8&type=recovery&redirect_to=http://127.0.0.1:3000/auth/update )

Alternatively, enter the code: 657886
Clicking the link redirects correctly and appends a ?code= param to the end. This is where I'm stuck - I assumed I should be calling exchangeCodeForSession with the code param since it seems to be auto-applied to the redirect, but I end up with the following error:
TypeError - Cannot read properties of null (reading 'split')
TypeError - Cannot read properties of null (reading 'split')
One thing I did notice was that the email template for Reset Password (in my supabase dashboard, not local) is different than what this flow is offering. So at this point, my question is: Is the template being sent the wrong template? I would expect the reset password method to send the right URL for the flow. If it is the wrong template/link, how do I modify it to match what I have in my project dashboard? I found this Github issue with the same error but not much else while looking around on discord/github/stackoverflow: https://github.com/supabase/supabase/issues/19896
GitHub
Cannot read properties of null (reading 'split') in exchangeCodeFor...
Bug report I confirm this is a bug with Supabase, not with my own application. I confirm I have searched the Docs, GitHub Discussions, and Discord. Describe the bug I am seeing TypeError - Cannot r...
4 Replies
dave
daveOP2y ago
Just an update here - I figured out how to set up a reset password email locally and have it matching what's in prod - the verifyOtp method works - the updateUser method does not work When calling updateUser - after going through verifyingOtp and setting a session, I get error: AuthSessionMissingError: Auth session missing! I was finally able to get this to work by calling setSession directly before updateUser
await supabase.auth.setSession({
access_token: session.get("access_token") as string,
refresh_token: session.get("refresh_token") as string,
});

const {
data: { user },
error,
} = await supabase.auth.updateUser({
password: new_password as string,
});
await supabase.auth.setSession({
access_token: session.get("access_token") as string,
refresh_token: session.get("refresh_token") as string,
});

const {
data: { user },
error,
} = await supabase.auth.updateUser({
password: new_password as string,
});
garyditsch
garyditsch2y ago
Would you be willing to share more of the sign up and login flows you've set up? I've been working on using the new supbase/ssr library to set up a new project and having a challenge at getting it all wired up. This is my first project with both Remix and Supabase, so while most things are normal learning curve, this part (with the new ssr libary and auth) is a big sticking point for me. I have been successful getting the sign-in, sign-out flow to work. I'll try and share some tips that worked for me, once I've better understood all that I have.
dave
daveOP2y ago
@garyditsch sure thing, I can share samples later tonight or tomorrow Got delayed because of the holidays. Here's signup and login files I have working right now. I'm omitting some of the imports for brevity's sake. login.ts
import { commitSession, getSession } from "~/utils/sessions.ts";
import { getSupabaseWithHeaders } from "~/utils/supabase.server.ts";

export const action = async ({ request }: ActionFunctionArgs) => {
const sessionStorage = await getSession(request.headers.get("Cookie"));
const { supabase } = getSupabaseWithHeaders({ request });
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");

const {
data: { user, session },
} = await supabase.auth.signInWithPassword({
email: email as string,
password: password as string,
});

if (session && user) {
sessionStorage.set("access_token", session.access_token);
sessionStorage.set("refresh_token", session.refresh_token);
sessionStorage.set("current_user", user.id);

return redirect("/dashboard", {
headers: {
"Set-Cookie": await commitSession(sessionStorage),
},
});
}
};

export default function Login() {
return (
<Container>
<Card>
<CardHeader className="space-y-1">
<CardTitle className="text-2xl">Login</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<Form method="post">
<div className="grid gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
placeholder="m@example.com"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" name="password" />
</div>
</div>
<Button type="submit" className="w-full">
Login
</Button>
</Form>
<p>
<Link to="/forgot">Forgot password?</Link>
</p>
</CardContent>
</Card>
</Container>
);
}
import { commitSession, getSession } from "~/utils/sessions.ts";
import { getSupabaseWithHeaders } from "~/utils/supabase.server.ts";

export const action = async ({ request }: ActionFunctionArgs) => {
const sessionStorage = await getSession(request.headers.get("Cookie"));
const { supabase } = getSupabaseWithHeaders({ request });
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");

const {
data: { user, session },
} = await supabase.auth.signInWithPassword({
email: email as string,
password: password as string,
});

if (session && user) {
sessionStorage.set("access_token", session.access_token);
sessionStorage.set("refresh_token", session.refresh_token);
sessionStorage.set("current_user", user.id);

return redirect("/dashboard", {
headers: {
"Set-Cookie": await commitSession(sessionStorage),
},
});
}
};

export default function Login() {
return (
<Container>
<Card>
<CardHeader className="space-y-1">
<CardTitle className="text-2xl">Login</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<Form method="post">
<div className="grid gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
placeholder="m@example.com"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" name="password" />
</div>
</div>
<Button type="submit" className="w-full">
Login
</Button>
</Form>
<p>
<Link to="/forgot">Forgot password?</Link>
</p>
</CardContent>
</Card>
</Container>
);
}
For some reason, I found the Supabase session to be unreliable but Remix sessions work very consistently for me. So I'm just using supabase for the API and then storing everything in the Remix Session. The getSupabaseWithHeaders method is in my original post. Signup action and loader look like this:
import { authenticate } from "~/utils/authenticate.ts";

export const loader = async ({ request }: LoaderFunctionArgs) => {
const { token } = await authenticate({ request });
const { headers } = getSupabaseWithHeaders({ request });

if (token) {
return redirect("/dashboard", { headers });
}

return json({ success: true }, { headers });
};

export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
const sessionStorage = await getSession(request.headers.get("Cookie"));
const { supabase } = getSupabaseWithHeaders({ request });

const {
data: { session },
} = await supabase.auth.signUp({
email: email as string,
password: password as string,
});

if (session) {
sessionStorage.set("access_token", session.access_token);
sessionStorage.set("refresh_token", session.refresh_token);

return redirect("/dashboard", {
headers: {
"Set-Cookie": await commitSession(sessionStorage),
},
});
}

return redirect("/");
};
import { authenticate } from "~/utils/authenticate.ts";

export const loader = async ({ request }: LoaderFunctionArgs) => {
const { token } = await authenticate({ request });
const { headers } = getSupabaseWithHeaders({ request });

if (token) {
return redirect("/dashboard", { headers });
}

return json({ success: true }, { headers });
};

export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
const sessionStorage = await getSession(request.headers.get("Cookie"));
const { supabase } = getSupabaseWithHeaders({ request });

const {
data: { session },
} = await supabase.auth.signUp({
email: email as string,
password: password as string,
});

if (session) {
sessionStorage.set("access_token", session.access_token);
sessionStorage.set("refresh_token", session.refresh_token);

return redirect("/dashboard", {
headers: {
"Set-Cookie": await commitSession(sessionStorage),
},
});
}

return redirect("/");
};
The authenticate util just checks if the following exists
const refreshToken = sessionStorage.get("refresh_token");
const accessToken = sessionStorage.get("access_token");
const refreshToken = sessionStorage.get("refresh_token");
const accessToken = sessionStorage.get("access_token");
If access_token exists, I short circuit and return the token; if there's only a refresh token present, I refresh the session, store it on success and return the access token:
const { data, error } = await supabase.auth.refreshSession({
refresh_token: refreshToken,
});

if (refreshToken && !accessToken) {
const { data, error } = await supabase.auth.refreshSession({
refresh_token: refreshToken,
});

if (!error && data.session) {
sessionStorage.set("access_token", data.session.access_token);
sessionStorage.set("refresh_token", data.session.refresh_token);

request.headers.append("Set-Cookie", await commitSession(sessionStorage));

return { token: data.session.access_token };
}
}
const { data, error } = await supabase.auth.refreshSession({
refresh_token: refreshToken,
});

if (refreshToken && !accessToken) {
const { data, error } = await supabase.auth.refreshSession({
refresh_token: refreshToken,
});

if (!error && data.session) {
sessionStorage.set("access_token", data.session.access_token);
sessionStorage.set("refresh_token", data.session.refresh_token);

request.headers.append("Set-Cookie", await commitSession(sessionStorage));

return { token: data.session.access_token };
}
}
and then otherwise just return a {token: null} or redirect based on whatever condition I pass in
garyditsch
garyditsch2y ago
Thank you for coming back to this. I have had things mostly working, I will cross reference my work with this to evaluate my choices.

Did you find this page helpful?