DEV Community

Cover image for Mobile-First Approach for FastAPI Full-Stack Template Authentication: Migrating to phone_number/OTP
Javad Zarezadeh
Javad Zarezadeh

Posted on

Mobile-First Approach for FastAPI Full-Stack Template Authentication: Migrating to phone_number/OTP

As you may know, FastAPI is one of the most admired frameworks for developing RESTful APIs. Another fantastic project by the same author, @tiangolo, is the Full Stack FastAPI Template, which I previously wrote about here.

In this post, I'll guide you through the process of replacing the email/password authentication flow in the template with a phone_number/OTP-based system. This approach is ideal for mobile-first applications and offers a user-friendly, secure way to authenticate users. My goal is to make minimal changes to the original project while maintaining its adherence to OAuth2 and JWT standards. Let’s dive in! 😉


1. Replace email and password with phone_number and otp

Update Configuration

File: ./.env
Change Needed:

  • Update the FIRST_SUPERUSER value to a phone number, e.g., +17375550100 or +989130000000.
  • Remove FIRST_SUPERUSER_PASSWORD as it is no longer necessary.

Update the Models

File: ./backend/app/models.py
Changes Needed:

Replace all instances of the email field with phone_number.

Example:

email: EmailStr = Field(unique=True, index=True, max_length=255)
Enter fullscreen mode Exit fullscreen mode

Change to:

phone_number: str = Field(unique=True, index=True, max_length=20)
Enter fullscreen mode Exit fullscreen mode

⚠️ Important: This change requires updating the database schema using Alembic migrations.


Update API Routes

Private Routes

File: ./backend/app/api/routes/private.py
Replace all occurrences of email and password with phone_number and otp.

Login Route

File: ./backend/app/api/routes/login.py

Update the authenticate call:

user = crud.authenticate(session=session, email=form_data.username, password=form_data.password)
Enter fullscreen mode Exit fullscreen mode

Change to:

user = crud.authenticate(session=session, phone_number=form_data.username, otp=form_data.password)
Enter fullscreen mode Exit fullscreen mode

⚠️ Note: Keep form_data.username and form_data.password unchanged due to OAuth2 standards.


Update User CRUD

File: ./backend/app/api/routes/users.py

  • Replace email with phone_number.
  • Remove the if statement in the create_user function related to email validation.

Database Utilities

File: ./backend/app/core/db.py

  • Replace email references with phone_number.
  • Remove FIRST_SUPERUSER_PASSWORD as it is no longer necessary.

CRUD Operations

File: ./backend/app/crud.py

  • Replace all email and password references with phone_number and otp.
  • Rename get_user_by_email to get_user_by_phone_number and update all references to this function.

2. Add an API Endpoint to Request OTP

File: ./backend/app/api/routes/login.py

Add the following endpoint:

OTP_EXPIRE_MINUTES = 5

@router.post("/login/request-otp")
def request_otp(session: SessionDep, phone_number: str) -> Message:
    """
    Generate and send OTP to the provided phone number.
    """
    user = crud.get_user_by_phone_number(session=session, phone_number=phone_number)
    if not user:
        user = crud.create_user(session=session, user_create=UserCreate(phone_number=phone_number))

    otp = generate_otp()
    otp_expires_at = datetime.now(timezone.utc) + timedelta(minutes=OTP_EXPIRE_MINUTES)

    crud.update_user(
        session=session,
        db_user=user,
        user_in=UserUpdate(otp=otp, otp_expires_at=otp_expires_at),
    )

    send_otp(phone_number, otp)

    return Message(message="OTP sent successfully.")
Enter fullscreen mode Exit fullscreen mode

3. Nullify OTP After Login

File: ./backend/app/api/routes/login.py
In the login_access_token function, nullify the OTP after successful login:

crud.update_user(
    session=session,
    db_user=user,
    user_in=UserUpdate(otp=None, otp_expires_at=None)
)
Enter fullscreen mode Exit fullscreen mode

4. Remove Unnecessary Functions

File: ./backend/app/api/routes/login.py

Remove the following functions:

  • recover_password
  • reset_password
  • recover_password_html_content

5. Remove Unnecessary Email Features

File: ./backend/app/api/routes/users.py

Remove the email-related logic, such as:

if settings.emails_enabled and user_in.email: ...
Enter fullscreen mode Exit fullscreen mode

6. Remove Password Update and User Registration Functions

File: ./backend/app/api/routes/users.py

Remove the following functions:

  • update_password_me
  • register_user

Since we are now using OTP-based authentication, these functions are redundant.


7. Simplify Models

File: ./backend/app/models.py

Replace password with otp:

Update all password fields to otp.

Example:

password: str | None = Field(default=None, min_length=8, max_length=40)
Enter fullscreen mode Exit fullscreen mode

Change to:

otp: str | None = Field(default=None, max_length=6)
Enter fullscreen mode Exit fullscreen mode

Remove OTP from UserCreate:

For the UserCreate model, you don’t need to include the otp field. Update it as follows:

class UserCreate(UserBase):
    pass
Enter fullscreen mode Exit fullscreen mode

This simplifies the creation process since OTP will be generated later during login.


Add OTP Expiry Fields:

Add an otp_expires_at field to both the UserUpdate and User models.

Example:

otp_expires_at: datetime | None = Field(default=None, nullable=True)
Enter fullscreen mode Exit fullscreen mode

Remove Unnecessary Models:

Delete models that are no longer needed, including:

  • UserRegister
  • UpdatePassword
  • NewPassword

This cleanup ensures the models remain relevant to the new authentication system.


8. Update Utilities

File: ./backend/app/utils.py

Remove unused functions:

  • generate_reset_password_email
  • generate_new_account_email
  • generate_password_reset_token
  • verify_password_reset_token

Add OTP Generation:

def generate_otp():
    return str(random.randint(100000, 999999))  # 6-digit OTP
Enter fullscreen mode Exit fullscreen mode

Add SMS Sending:

def send_otp(phone_number: str, otp: str):
    """
    Sends an OTP to the provided phone number.
    """
    # Implement this function according to your SMS provider

    pass
Enter fullscreen mode Exit fullscreen mode

Following these steps will transform the Full Stack FastAPI Template’s email/password flow into a phone_number/OTP-based system while keeping it aligned with best practices and standards. Happy coding! 🚀


These changes to the original project are available in my GitHub. It is important to use this project cautiously, since I have not yet had time to write the tests.

Top comments (0)