Skip to content

Starlette-Login

Repository: Starlette-Login

Starlette-Login provides user session management for Starlette.

Much inspired by Flask-Login, it handles the common tasks of logging in, logging out, and remembering your users' sessions over extended periods of time.

Key features:

  • Store the active user’s ID in the session, and let you log them in and out easily.
  • Restrict routes to logged-in user only
  • Handle remember-me functionality

Installation

Stable

pip install Starlette-Login

Development

pip install 'git+https://github.com/jockerz/Starlette-Login'

Basic Example

To run example below, please install uvicorn.

pip install uvicorn

Models

User model and how to get the user by username and id

models.py
import typing
from dataclasses import dataclass

from starlette.requests import Request
from starlette_login.mixins import UserMixin


@dataclass
class User(UserMixin):
    identifier: int
    username: str
    password: str = 'password'

    def check_password(self, password: str):
        return self.password == password

    @property
    def is_authenticated(self) -> bool:
        return True

    @property
    def display_name(self) -> str:
        return self.username

    @property
    def identity(self) -> int:
        return self.identifier


class UserList:
    def __init__(self):
        self.user_list = []

    def dict_username(self) -> dict:
        d = {}
        for user in self.user_list:
            d[user.username] = user
        return d

    def dict_id(self) -> dict:
        d = {}
        for user in self.user_list:
            d[user.identity] = user
        return d

    def add(self, user: User) -> bool:
        if user.identity in self.dict_id():
            return False
        self.user_list.append(user)
        return True

    def get_by_username(self, username: str) -> typing.Optional[User]:
        return self.dict_username().get(username)

    def get_by_id(self, identifier: int) -> typing.Optional[User]:
        return self.dict_id().get(identifier)

    def user_loader(self, request: Request, user_id: int):
        return self.get_by_id(user_id)


user_list = UserList()
user_list.add(User(identifier=1, username='user1', password='password'))
user_list.add(User(identifier=2, username='user2', password='password'))

Routes

Implement login and logout utils and login_required decorator

routes.py
from urllib.parse import parse_qsl

from starlette.requests import Request
from starlette.responses import (
    RedirectResponse, HTMLResponse, PlainTextResponse
)

from starlette_login.decorator import login_required
from starlette_login.utils import login_user, logout_user

from models import user_list


HOME_PAGE = "You are logged in as <strong>{username}</strong>"
LOGIN_PAGE = """
<h4>{error}<h4>
<form method="POST">
<label>username <input name="username"></label>
<label>Password <input name="password" type="password"></label>
<button type="submit">Login</button>
</form>
"""


async def login_page(request: Request):
    error = ''
    if request.user.is_authenticated:
        return RedirectResponse('/', 302)
    if request.method == 'POST':
        body = (await request.body()).decode()
        data = dict(parse_qsl(body))
        user = user_list.get_by_username(data['username'])
        if not user:
            error = 'Invalid username'
        elif user.check_password(data['password']) is False:
            error = 'Invalid password'
        else:
            await login_user(request, user)
            return RedirectResponse('/', 302)
    return HTMLResponse(LOGIN_PAGE.format(error=error))


async def logout_page(request: Request):
    if request.user.is_authenticated:
        content = 'Logged out'
        await logout_user(request)
    else:
        content = 'You not logged in'
    return PlainTextResponse(content)


async def home_page(request: Request):
    if request.user.is_authenticated:
        content = HOME_PAGE.format(username=request.user.username)
    else:
        content = 'You are not logged in'
    return HTMLResponse(content=content)


@login_required
async def protected_page(request: Request):
    return PlainTextResponse(f'You are logged in as {request.user.username}')

Starlette Application

Using AuthenticationMiddleware, then using the LoginManager and set LoginManager's user_loader callback function by set_user_loader method.

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.routing import Route

from starlette_login.backends import SessionAuthBackend
from starlette_login.login_manager import LoginManager
from starlette_login.middleware import AuthenticationMiddleware

from models import user_list
from routes import home_page, login_page, logout_page, protected_page


login_manager = LoginManager(redirect_to='login', secret_key='secret')
login_manager.set_user_loader(user_list.user_loader)

app = Starlette(
    middleware=[
        Middleware(SessionMiddleware, secret_key='secret'),
        Middleware(
            AuthenticationMiddleware,
            backend=SessionAuthBackend(login_manager),
            login_manager=login_manager,
            allow_websocket=False,
        )
    ],

    routes=[
        Route('/', home_page, name='home'),
        Route('/login', login_page, methods=['GET', 'POST'], name='login'),
        Route('/logout', logout_page, name='logout'),
        Route('/protected', protected_page, name='protected'),
    ],
)
app.state.login_manager = login_manager

Run

uvicorn app:app

Endpoints

  • Home: /
  • Login: /login
  • Logout: /logout

More Examples