The Login Endpoint

Add a POST /login endpoint that verifies credentials and returns a JWT

💻

Writing code and entering commands is only available on desktop. Open this page on a larger screen to complete this chapter.

From registration to login

Registration creates the account. Login verifies the credentials and hands back a token. The login endpoint takes the same email and password, but instead of storing a new user, it looks up an existing one and checks the password.

Why the error message is vague

When login fails, the endpoint returns "Invalid email or password" — the same message whether the email does not exist or the password is wrong. This is intentional. A specific message like "Email not found" tells an attacker which emails exist in your database. A vague message reveals nothing.

Two new imports from auth

The login endpoint uses two functions from auth.py that you have not imported yet: verify_password checks the plain password against the stored hash, and create_access_token generates the JWT. You will add both to the existing auth import line.

Instructions

Add a login endpoint to main.py.

  1. Update the auth import to include the two new functions you need: from auth import User, hash_password, verify_password, create_access_token.
  2. Define the login endpoint with @app.post("/login") and def login(user_data: UserCreate, session: SessionDep): — it reuses UserCreate since login takes the same email and password fields as registration.
  3. Look up the user by email: user = session.exec(select(User).where(User.email == user_data.email)).first() — if no account exists with this email, user will be None.
  4. Check both failure cases in one condition: if user is None or verify_password(user_data.password, user.password_hash) returns False, raise HTTPException(status_code=401, detail="Invalid email or password"). This handles both "email not found" and "wrong password" with the same vague message — as explained in the content above, you do not want to reveal which one failed.
  5. If the credentials are valid, generate a JWT: token = create_access_token(user.email) — this calls the function you built in Lesson 1 that signs the user's email into a 30-minute token.
  6. Return {"access_token": token, "token_type": "bearer"} — this is the standard OAuth2 response format. The "token_type": "bearer" tells the client to send the token in an Authorization: Bearer header.