I'm having this error AuthSessionMissingError: Auth session missing!
on local development when i tried to update user password on my password reset flow my app is built on nextjs. I had recieved the email with the url configured to redirect but it showed like this:
http://localhost:8081/update-password#access_token=eyJhb...&expires_in=3600&refresh_token=QhVGnqZytUsjj946C1...&token_type=bearer&type=recovery
As you can see i havent received any code on the url.
For the pasword recovery initial flow i did:
const sendRecoveryEmail = async():Promise<void> => {
setIsSending(true);
let url = process.env.NODE_ENV === "development" ? `http://localhost:8081/update-password` : `https://www.somsite.com/update-password`;
if(email === "") {
emptyEmailToaster();
return;
}
const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: url,
});
setIsSending(false);
recoveryEmailSentToaster();
};
and for updating the user password:
const [isSmallerScreenMobile] = useMediaQuery('(max-width: 500px)');
const [password, setPassword] = useState<string | "">("");
const [passwordConfirmation, setPasswordConfirmation] = useState<string | "">("");
const [isUpdating, setIsUpdating] = useState<boolean>(false);
const handleInfo = (e:ChangeEvent<HTMLInputElement>):void => {
const { name, value } = e.target;
setPassword(value);
};
useEffect(() => {
}, []);
const handleInfoPasswordConfirmation = (e:ChangeEvent<HTMLInputElement>):void => {
const { name, value } = e.target;
setPasswordConfirmation(value);
};
const emptyPasswordToaster = () => {
const toastOptions:ToastOptions = {
title: 'Debes colocar la nueva contraseña.',
description: "",
status: 'error',
duration: 1000,
isClosable: true,
}
return toast(toastOptions);
}
const passwordDoesntMarchToaster = () => {
const toastOptions:ToastOptions = {
title: 'Las contraseñas no coinciden.',
description: "",
status: 'error',
duration: 1000,
isClosable: true,
}
return toast(toastOptions);
}
const passwordLengthToaster = () => {
const toastOptions:ToastOptions = {
title: 'Las contraseña debe tener al menos 8 caracteres.',
description: "",
status: 'error',
duration: 1000,
isClosable: true,
}
return toast(toastOptions);
}
const updateUserPassword = async():Promise<void> => {
setIsUpdating(true);
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
console.log(session)
console.log(sessionError)
// if (sessionError || !session) {
// console.log('Session error or no session found');
// setIsUpdating(false);
// return;
// }
if(password.length < 8) {
passwordLengthToaster();
setIsUpdating(false);
return;
}
if(password === '') {
emptyPasswordToaster();
setIsUpdating(false);
return;
}
if(password !== passwordConfirmation) {
passwordDoesntMarchToaster();
setIsUpdating(false);
return;
};
try {
const { data, error } = await supabase.auth.updateUser({
password: password,
});
setIsUpdating(false);
console.log(data);
console.log(error);
} catch (err) {
console.log('There was an error trying to update the password ', err);
}
};
I was able to update the password through a workaround. The only issue I'm facing is that I can't delete sb-access-token and sb-refresh-token. What I did was:
http://localhost:8081/update-password#access_token=eyJhb...&expires_in=3600&refresh_token=QhVGnqZytUsjj946C1...&token_type=bearer&type=recovery
)Note: i couldnt remove the access_token
and refresh_token
through a function:
const getUrlFragmentParams = (url: string):void => {
const params: { [key: string]: string } = {};
const hash = new URL(url).hash.substring(1);
const pairs = hash.split('&');
pairs.forEach(pair => {
const [key, value] = pair.split('=');
if (key && value) {
params[key] = decodeURIComponent(value);
}
});
setAccessToken(params.access_token);
setRefreshToken(params.refresh_token);
createSession(params.access_token, params.refresh_token);
};
createSession
function was called and those variables described above in step one were passed to it. const createSession = async(access_token:string, refresh_token:string):Promise<void> => {
const { data, error } = await supabase.auth.setSession({
access_token,
refresh_token
});
}
supabase.auth.updateUser
function. Along with that, all cookies are deleted and the logout
function (stored in the context) is also called to close the session. const updateUserPassword = async():Promise<void> => {
setIsUpdating(true);
if(password.length < 8) {
passwordLengthToaster();
setIsUpdating(false);
return;
}
if(password === '') {
emptyPasswordToaster();
setIsUpdating(false);
return;
}
if(password !== passwordConfirmation) {
passwordDoesntMarchToaster();
setIsUpdating(false);
return;
};
try {
const { data, error } = await supabase.auth.updateUser({
password: password,
});
setIsUpdating(false);
if(data && Object.keys(data).length > 0) {
setTimeout(() => {
passwordUpdatedToaster();
deleteAllCookies();
logout();
}, 1000);
}
console.log(error);
} catch (err) {
console.log('There was an error trying to update the password ', err);
}
};
The only thing I have doubts about this workaround is that the sb-access-token
and sb-refresh-token
cookies are not deleted when the function is called to delete them.
const deleteAllCookies = ():void => {
document.cookie.split(';').forEach(cookie => {
const eqPos = cookie.indexOf('=');
const name = eqPos > -1 ? cookie.substring(0, eqPos).trim() : cookie.trim();
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
});
const cookiesToDelete = ['sb-access-token', 'sb-refresh-token'];
cookiesToDelete.forEach(name => {
// Delete the cookie by setting its expiration date to the past
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
});
}
My questions are the following: 1 - Is this workaround unsafe? 2 - Will those tokens be deleted between 5 minutes and an hour?
I attach the final complete code:
'use client';
// React
import React, { ChangeEvent, useState, useEffect } from "react";
// Supabase
import { createClient } from '../../utils/supabase/client';
// Nextjs
import { useSearchParams } from 'next/navigation';
// Context
import { useAuth } from "../../context/AuthContext";
// Chakra-ui
import {
Box,
Button,
Flex,
FormControl,
FormLabel,
Heading,
Input,
Stack,
Text,
Link,
Spinner,
useColorModeValue,
useMediaQuery,
useToast
} from '@chakra-ui/react';
interface ToastOptions {
title: string;
description: string;
status: 'success' | 'error' | 'warning'
duration?: number;
isClosable?: boolean;
}
const UpdatePassword = ():React.ReactNode => {
const toast = useToast();
const supabase = createClient();
const searchParams = useSearchParams();
const [isSmallerScreenMobile] = useMediaQuery('(max-width: 500px)');
const [password, setPassword] = useState<string | "">("");
const [accessToken, setAccessToken] = useState<string | "">("");
const [refreshToken, setRefreshToken] = useState<string | "">("");
const [passwordConfirmation, setPasswordConfirmation] = useState<string | "">("");
const [isUpdating, setIsUpdating] = useState<boolean>(false);
const { logout } = useAuth();
const handleInfo = (e:ChangeEvent<HTMLInputElement>):void => {
const { name, value } = e.target;
setPassword(value);
};
useEffect(() => {
getUrlFragmentParams(window.location.href)
}, []);
const getUrlFragmentParams = (url: string):void => {
const params: { [key: string]: string } = {};
const hash = new URL(url).hash.substring(1);
const pairs = hash.split('&');
pairs.forEach(pair => {
const [key, value] = pair.split('=');
if (key && value) {
params[key] = decodeURIComponent(value);
}
});
setAccessToken(params.access_token);
setRefreshToken(params.refresh_token);
createSession(params.access_token, params.refresh_token);
};
const createSession = async(access_token:string, refresh_token:string):Promise<void> => {
const { data, error } = await supabase.auth.setSession({
access_token,
refresh_token
});
}
const handleInfoPasswordConfirmation = (e:ChangeEvent<HTMLInputElement>):void => {
const { name, value } = e.target;
setPasswordConfirmation(value);
};
const emptyPasswordToaster = () => {
const toastOptions:ToastOptions = {
title: 'Debes colocar la nueva contraseña.',
description: "",
status: 'error',
duration: 1000,
isClosable: true,
}
return toast(toastOptions);
}
const passwordDoesntMarchToaster = () => {
const toastOptions:ToastOptions = {
title: 'Las contraseñas no coinciden.',
description: "",
status: 'error',
duration: 1000,
isClosable: true,
}
return toast(toastOptions);
}
const passwordLengthToaster = () => {
const toastOptions:ToastOptions = {
title: 'Las contraseña debe tener al menos 8 caracteres.',
description: "",
status: 'error',
duration: 1000,
isClosable: true,
}
return toast(toastOptions);
}
const passwordUpdatedToaster = () => {
const toastOptions:ToastOptions = {
title: 'Nueva contraseña cambiada.',
description: "",
status: 'success',
duration: 1000,
isClosable: true,
}
return toast(toastOptions);
}
const deleteAllCookies = ():void => {
document.cookie.split(';').forEach(cookie => {
const eqPos = cookie.indexOf('=');
const name = eqPos > -1 ? cookie.substring(0, eqPos).trim() : cookie.trim();
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
});
const cookiesToDelete = ['sb-access-token', 'sb-refresh-token'];
cookiesToDelete.forEach(name => {
// Delete the cookie by setting its expiration date to the past
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
});
}
const updateUserPassword = async():Promise<void> => {
setIsUpdating(true);
if(password.length < 8) {
passwordLengthToaster();
setIsUpdating(false);
return;
}
if(password === '') {
emptyPasswordToaster();
setIsUpdating(false);
return;
}
if(password !== passwordConfirmation) {
passwordDoesntMarchToaster();
setIsUpdating(false);
return;
};
try {
const { data, error } = await supabase.auth.updateUser({
password: password,
});
setIsUpdating(false);
if(data && Object.keys(data).length > 0) {
setTimeout(() => {
passwordUpdatedToaster();
deleteAllCookies();
logout();
}, 1000);
}
console.log(error);
} catch (err) {
console.log('There was an error trying to update the password ', err);
}
};
return(
<Flex
justifyContent={'center'}
alignItems={'center'}
h='80vh'>
<Box w={isSmallerScreenMobile ? '90%' : '40%'}>
<Stack spacing={'8px'}>
<Text fontSize={'100%'} textAlign={'center'} fontWeight={'700'}>Nueva Contraseña</Text>
<Text textAlign='center' fontSize={'18px'}>Escribe tu nueva contraseña para terminar el proceso.</Text>
<Input
onChange={(e) => handleInfo(e)}
name='password'
type='password'
placeholder="Ingresa tu contraseña"/>
<Input
onChange={(e) => handleInfoPasswordConfirmation(e)}
name='password_confirmation'
type='password'
placeholder="Confirmación de contraseña"/>
<Button onClick={() => updateUserPassword()} colorScheme="green">
<Text>{isUpdating ? <Spinner /> : 'Actualizar Contraseña'}</Text>
</Button>
</Stack>
</Box>
</Flex>
)
};
export default UpdatePassword;