Search code examples
supabase

Supabase getting error when reseting the password


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);
    }
  };


Solution

  • 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:

    1. Through the redirect URL (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);
      };
    
    1. After obtaining these values, the 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
        });
      }
    
    1. Once logged in, I was able to update the password with the 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;