Search code examples
pythonfastapiforgot-passwordpyotp

pyotp couldn't pass the same secret key to multiple methods


I am implementing a forgot password feature on my application. In order to generate the otp code I have used the pyotp library. The code generates the otp code but when I try to reset the password using the generated password, it shows the error that "the verification code has beeen expired" but the otp code here has the expiry time of 180 seconds. I think the problem is in the way I am generating the secret key. I guess the reset password method expects the same key that was used while creating the otp code. I want to resend the new code everytime the user hits the endpoint, even if the code is valid. How can I pass the same secret key to the reset password method? Here is my implementation code:

  1. Method to generate the otp code:
    secret = pyotp.random_base32()
    totp = pyotp.TOTP(secret, interval=settings.verification_code_expire_time)
    verification_code = totp.now()
    if not user.code:
        user.code = VerificationCode(code=verification_code)
    else:
        user.code.code = verification_code
    await db.commit()
    return verification_code
  1. Method to reset password:
    query = select(VerificationCode).where(VerificationCode.code == reset_password_schema.verification_code)
    result = await db.execute(query)
    verification_code = result.scalar_one_or_none()

    if not verification_code:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid verification code.")

    totp = pyotp.TOTP(pyotp.random_base32(), interval=settings.verification_code_expire_time)

    if not totp.verify(reset_password_schema.verification_code):
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Verification code has been expired.")

    query = (
        update(User).where(User.id == verification_code.user_id).values(
            password=reset_password_schema.new_password)
    ) 

Solution

  • You generate new secret key at your Method to reset password , that's why this happens. Instead, you should use secret key from created VerificationCode. Here is simple example:

    1. First of all, you need to save this secret key:
    secret = pyotp.random_base32()
    totp = pyotp.TOTP(secret,interval=settings.verification_code_expire_time)
    verification_code = totp.now()
    
    if not user.code:
        user.code = VerificationCode(code=verification_code, secret=secret)
    else:
        user.code.code = verification_code
        user.code.secret = secret # SAVE THIS ENCRYPTED
    
    await db.commit()
    return verification_code
    

    P.S. You really should store this secret encrypted

    1. Get your secret key and use it in Method to reset password:
    query = select(VerificationCode).where(VerificationCode.code == reset_password_schema.verification_code)
    result = await db.execute(query)
    verification_code_entry= result.scalar_one_or_none()
    
    if not verification_code_entry:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail="Invalid verification code.")
    
    # HERE IS MAIN CHANGE
    secret = verification_code_entry.secret
    totp= pyotp.TOTP(secret,interval=settings.verification_code_expire_time)
    
    if not totp.verify(reset_password_schema.verification_code):
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Verification code has expied.")
    
    query = (
        update(User)
        .where(User.id == verification_code_entry.user_id)
        .values(password=reset_password_schema.new_password)
    )
    await db.execute(query)
    await db.commit()