TausiDocs

Python: Django, Django REST framework and Flask

Python 3.9+, no runtime dependencies. Protect an API with the secret key, or sign users in from server-rendered pages with the publishable key; the token stays in the server-side session.

Install

tausi on PyPIRequires Python 3.9+; no dependencies
Shell
pip install "tausi[django]"   # Django and Django REST framework
pip install "tausi[flask]"    # Flask
pip install tausi             # FastAPI and anything else

Settings come from your framework config or the environment: TAUSI_URL, TAUSI_SECRET_KEY, and TAUSI_PUBLISHABLE_KEY (only for server-rendered sign-in). TAUSI_TIMEOUT sets the request timeout in seconds.

Django: protect views

settings.pyPython
TAUSI_URL = "https://auth.tausi.app"
TAUSI_SECRET_KEY = os.environ["TAUSI_SECRET_KEY"]

MIDDLEWARE = [
    # ...
    "tausi.django.TausiAuthMiddleware",   # optional: sets request.tausi on every request
]
views.pyPython
from django.http import JsonResponse
from tausi.django import tausi_optional, tausi_required

@tausi_required                      # 401 JSON without a valid Bearer token; async views work too
def me(request):
    return JsonResponse(request.tausi.user.to_dict())

@tausi_optional                      # request.tausi is an AuthResult or None
def feed(request):
    ...

Only requests that carry a bearer token cost a call to Tausi. If Tausi can't be reached the decorators and middleware answer 503 with code AUTH_UNAVAILABLE instead of signing people out.

Django REST framework

settings.pyPython
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ["tausi.django.rest_framework.TausiAuthentication"],
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
}
views.pyPython
class Me(APIView):
    def get(self, request):
        # request.user is a tausi.User (is_authenticated is True); request.auth is the AuthResult
        return Response({"id": request.user.id, "email": request.user.email})

Requests without a bearer token fall through to your other authentication classes.

Django: server-rendered sign-in

views.pyPython
from django.shortcuts import redirect, render
from tausi import TausiError
from tausi.django import get_client, tausi_login_required

# settings.py: TAUSI_PUBLISHABLE_KEY = os.environ["TAUSI_PUBLISHABLE_KEY"]  (needs SessionMiddleware)

def sign_in(request):
    if request.method == "POST":
        try:
            get_client(request).sign_in(request.POST["email"], request.POST["password"])
            request.session.cycle_key()
            return redirect(request.GET.get("next", "/"))
        except TausiError as e:
            return render(request, "sign_in.html", {"error": e.message, "code": e.code})
    return render(request, "sign_in.html")

def sign_up(request):
    try:
        get_client(request).sign_up(
            request.POST["email"], request.POST["password"], request.POST["name"],
            username=request.POST.get("username") or None,
            phone_number=request.POST.get("phone") or None,
        )
    except TausiError as e:
        if e.code == "EMAIL_ALREADY_REGISTERED":
            ...  # offer sign-in / password reset; the owner has been emailed

@tausi_login_required                 # redirects to settings.LOGIN_URL with ?next=
def dashboard(request):
    return render(request, "dashboard.html", {"user": request.tausi.user})

Also on get_client(request): sign_out, update_user(first_name=…, timezone=None), change_password, forgot_password, reset_password, verify_email, list_sessions, revoke_session, start_oauth and handle_oauth_redirect.

Flask

app.pyPython
from flask import Flask, g, redirect, render_template, request, session
from tausi import TausiError
from tausi.flask import Tausi

app = Flask(__name__)
app.config.update(TAUSI_URL="https://auth.tausi.app", TAUSI_SECRET_KEY=..., TAUSI_PUBLISHABLE_KEY=...)
tausi = Tausi(app)

@app.get("/api/me")
@tausi.required                       # 401 JSON without a valid Bearer token
def me():
    return g.tausi.user.to_dict()

@app.post("/sign-in")
def login():
    try:
        tausi.client().sign_in(request.form["email"], request.form["password"])   # token kept in flask.session
    except TausiError as e:
        return render_template("sign_in.html", error=e.message), 400
    return redirect(request.args.get("next", "/"))

@app.get("/dashboard")
@tausi.login_required(login_endpoint="login")
def dashboard():
    return render_template("dashboard.html", user=g.tausi.user)

@app.get("/login/google")
def google():
    start = tausi.client().start_oauth("google", redirect_to="https://app.example.com/auth/callback")
    session["tausi_verifier"] = start.code_verifier
    return redirect(start.url)

@app.get("/auth/callback")
def callback():
    tausi.client().handle_oauth_redirect(request.url, session.pop("tausi_verifier", ""))
    return redirect("/dashboard")
Flask sessions
Flask's default session is a signed (not encrypted) cookie. For server-side storage of the token, use a server-side session such as Flask-Session.

FastAPI and other frameworks

main.pyPython
from fastapi import Depends, FastAPI, HTTPException, Request
from starlette.concurrency import run_in_threadpool
from tausi import AuthResult, TausiBackend

tausi = TausiBackend("https://auth.tausi.app", os.environ["TAUSI_SECRET_KEY"])
app = FastAPI()

async def current_auth(request: Request) -> AuthResult:
    auth = await run_in_threadpool(tausi.authenticate_request, request)   # the SDK is synchronous
    if auth is None:
        raise HTTPException(401, "Sign in to continue.")
    return auth

@app.get("/me")
async def me(auth: AuthResult = Depends(current_auth)):
    return auth.user.to_dict()

authenticate_request() accepts any request with .headers, a headers mapping, or the header value. To use requests or httpx, pass transport=.

Manage users

admin.pyPython
from tausi.django import get_backend     # Flask: tausi.backend; elsewhere: TausiBackend(...)

backend = get_backend()
page = backend.users.list(q="asha", access="active", page_size=50)
for u in page.items:
    print(u.email, u.status, u.active_sessions, u.last_sign_in_at, u.access)

detail = backend.users.get(user_id)       # user, sessions (devices), signInHistory, recentAudit
backend.users.update(user_id, email_verified=True, public_metadata={"plan": "pro"}, private_metadata={"stripe": "cus_123"})
backend.users.suspend(user_id, hours=24, reason="Chargeback")
backend.users.ban(user_id, reason="Spam")
backend.users.unban(user_id)
backend.users.bulk("revoke_sessions", [id_a, id_b])
backend.users.delete(user_id)

See Managing users.

Errors

AttributeMeaning
e.codeStable error code, e.g. INVALID_CREDENTIALS, ACCOUNT_SUSPENDED. Branch on this.
e.statusHTTP status; 0 for NETWORK_ERROR and TIMEOUT.
e.messageHuman-readable text you can show.
e.detailsExtra data, e.g. {"issues": [{"path", "message"}]} for VALIDATION_ERROR.
e.retry_afterSeconds to wait, for RATE_LIMITED.
e.request_idQuote it when reporting a problem.