KAHIBARO
Discord Login Register

13.5. Login

Understanding Login in Backend Systems

Login is the process where a user proves who they are to your backend so that your system can create an authenticated session or issue tokens. In this chapter you will focus on what is unique about login compared to registration, and how to implement a secure login flow in a typical backend.

You will see examples in a style similar to Python and FastAPI, but the ideas apply to any backend stack.


What Makes Login Different From Registration

Registration creates an account. Login uses an existing account.

During registration, you:

During login, you:

Never store or compare plain text passwords. Always compare the password using a secure password hashing function against the stored hash.


Typical Login Flow

Here is the high-level login flow you will usually implement:

  1. Client sends credentials to POST /login.
  2. Backend validates input structure, for example email format.
  3. Backend finds the user record from the database by email or username.
  4. Backend verifies the password using a password hashing library.
  5. Backend performs additional checks, for example:
    • Is email verified?
    • Is the account disabled or locked?
  6. Backend issues:
    • A session cookie, or
    • An access token (and maybe a refresh token), or
    • Both tokens and cookies.
  7. Backend returns a response that indicates success or failure.

Here is a simple pseudo-code example:

python
@app.post("/login")
def login(data: LoginRequest):
    user = get_user_by_email(data.email)
    if not user:
        raise InvalidCredentialsError()
    if not verify_password(data.password, user.password_hash):
        raise InvalidCredentialsError()
    if not user.is_active:
        raise InactiveUserError()
    # For token-based auth:
    access_token = create_access_token({"sub": user.id})
    return {"access_token": access_token, "token_type": "bearer"}

The details of verify_password, create_access_token, and how tokens work are covered in other chapters, so here you focus on how to use them in the login process.


Designing the Login Endpoint

Choosing the HTTP Method and Path

Login is almost always:

You use POST because you are sending sensitive data that should not appear in URLs. Also, you are creating server-side state, like a session, or returning new tokens.

Request Body vs Query Parameters

Credentials should be sent in the request body, not in query parameters.

Bad:

http
POST /login?email=alice@example.com&password=secret123 HTTP/1.1

Good:

http
POST /login HTTP/1.1
Content-Type: application/json
{
  "email": "alice@example.com",
  "password": "secret123"
}

Using the body makes it easier to avoid logging credentials accidentally, and many HTTP clients treat bodies more securely than query strings.

Request Model Example

You can define a simple request model like:

python
class LoginRequest(BaseModel):
    email: EmailStr
    password: str

You might also accept username instead of email, or both:

python
class LoginRequest(BaseModel):
    username_or_email: str
    password: str

Validating Login Input

Even for login, validate the input structure before trying to authenticate.

Some examples:

Example with basic validation:

python
class LoginRequest(BaseModel):
    email: EmailStr
    password: constr(min_length=8, max_length=128)

If validation fails, you respond with a 400 Bad Request, not with an "invalid credentials" error, because this is not an authentication failure, it is an invalid request format.


Looking Up the User

Once the input is valid, you need to find the user record.

Example:

python
def authenticate_user(email: str, password: str) -> User | None:
    user = db.query(User).filter(User.email == email).first()
    if not user:
        return None
    if not verify_password(password, user.password_hash):
        return None
    return user

You typically:

Do not return different error messages for "user does not exist" and "wrong password". Use a single generic error for both.


Verifying the Password

The verification step uses a password hashing function. From the login perspective, there are only two key operations:

Example pattern:

python
def verify_password(plain_password: str, password_hash: str) -> bool:
    return pwd_context.verify(plain_password, password_hash)

You never compare the plain password string directly with anything from the database.

Timing and Security Considerations

An attacker might try to measure how long different responses take. To reduce information leaks:

Simplified example:

python
DUMMY_PASSWORD_HASH = "$2b$12$dummy..."
def authenticate_user(email: str, password: str) -> User | None:
    user = db.query(User).filter(User.email == email).first()
    if not user:
        # Fake verification to match timing.
        verify_password(password, DUMMY_PASSWORD_HASH)
        return None
    if not verify_password(password, user.password_hash):
        return None
    return user

Handling Additional Login Checks

Once the password is correct, you may need to enforce other business rules.

Common checks:

Example:

python
def login(data: LoginRequest):
    user = authenticate_user(data.email, data.password)
    if not user:
        raise InvalidCredentialsError()
    if not user.is_active:
        raise HTTPException(status_code=403, detail="Account is disabled")
    if settings.REQUIRE_EMAIL_VERIFIED and not user.is_email_verified:
        raise HTTPException(status_code=403, detail="Email not verified")
    # passed all checks, issue tokens or session

What to Return on Successful Login

The exact response depends on your chosen authentication method. Common patterns:

1. Session Cookie Based Login

Used heavily in traditional web apps:

Example response:

http
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax
Content-Type: application/json
{
  "message": "Logged in successfully"
}

From the login chapter viewpoint, your key task is to:

2. Token Based Login

Often used for APIs and mobile apps:

Example response body:

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 3600
}

From the login perspective:

3. Hybrid Approach

You might:

The login endpoint can vary based on client type, for example /web/login vs /api/login.


Handling Incorrect Logins Securely

How you handle failures is crucial for security.

Status Code

For incorrect credentials use:

A common pattern:

python
raise HTTPException(
    status_code=401,
    detail="Invalid email or password"
)

Do Not Reveal Which Field Was Wrong

You should never reveal:

Instead, use a generic message:

Limit Information in Error Messages

Avoid sending too much information in errors. For example, do not include:

Those details belong in your server logs, not in API responses.


Preventing Brute Force Attacks at Login

Login endpoints are a common target for attackers who try many passwords.

You can reduce risk by:

Example logic:

python
def login(data: LoginRequest):
    if too_many_attempts(data.email, client_ip):
        raise HTTPException(status_code=429, detail="Too many login attempts")
    user = authenticate_user(data.email, data.password)
    if not user:
        record_failed_login(data.email, client_ip)
        raise HTTPException(status_code=401, detail="Invalid email or password")
    clear_failed_logins(data.email, client_ip)
    # continue with success flow

Details of rate limiting and tracking failed attempts are covered in other security related chapters, but you should keep the pattern in mind as part of login design.


Example End to End Login Endpoint

Here is a more complete example that puts the pieces together for a token based login:

python
class LoginRequest(BaseModel):
    email: EmailStr
    password: constr(min_length=8, max_length=128)
class LoginResponse(BaseModel):
    access_token: str
    token_type: str
    expires_in: int
@app.post("/login", response_model=LoginResponse)
def login(data: LoginRequest, request: Request):
    client_ip = request.client.host
    # 1. Rate limiting
    if too_many_attempts(data.email, client_ip):
        raise HTTPException(status_code=429, detail="Too many login attempts")
    # 2. Authenticate user
    user = authenticate_user(data.email, data.password)
    if not user:
        record_failed_login(data.email, client_ip)
        raise HTTPException(status_code=401, detail="Invalid email or password")
    # 3. Additional checks
    if not user.is_active:
        raise HTTPException(status_code=403, detail="Account is disabled")
    # 4. Clear failed attempts on success
    clear_failed_logins(data.email, client_ip)
    # 5. Create access token
    expires_in = settings.ACCESS_TOKEN_EXPIRE_SECONDS
    access_token = create_access_token(
        {"sub": str(user.id)},
        expires_delta=timedelta(seconds=expires_in),
    )
    # 6. Return response
    return LoginResponse(
        access_token=access_token,
        token_type="bearer",
        expires_in=expires_in,
    )

This example shows how login coordinates:

Testing the Login Endpoint

You should test login carefully, because small mistakes can open serious vulnerabilities.

Create tests for:

ScenarioExpected Outcome
Correct email and password200 OK, valid token or session
Wrong email401, generic error, no detail about unknown email
Correct email, wrong password401, same generic error
Inactive or disabled user403, clear but non-revealing message
Too many failed attempts429, "Too many login attempts"
Invalid email format400, validation error
Very short password input400, validation error if you enforce min length

Example unit style test in Python:

python
def test_login_success(client, user_factory):
    user = user_factory(email="user@example.com", password="StrongPass123!")
    response = client.post("/login", json={
        "email": "user@example.com",
        "password": "StrongPass123!"
    })
    assert response.status_code == 200
    data = response.json()
    assert "access_token" in data
    assert data["token_type"] == "bearer"

Summary

In login you:

Registration, password hashing details, tokens, sessions, and security are explained in their own dedicated chapters, but you now know how all of them come together in the specific case of user login.

Views: 14

Comments

Please login to add a comment.

Don't have an account? Register now!