Skip to content

Python

Package: tenon-auth 1.0.0, Python 3.9+.

  • Dependencies: none; it uses the standard library.
  • Typing: it is typed (py.typed).
  • Thread safety: the client is thread-safe; create one per process.

Download the wheel from Downloads, check its SHA-256, and install it:

Terminal window
pip install tenon_auth-1.0.0-py3-none-any.whl
import os
from tenon_auth import TenonAuthClient
tenon_auth = TenonAuthClient(os.environ["TENON_AUTH_URL"], os.environ["TENON_AUTH_API_KEY"])

Options:

  • timeout (30 seconds);
  • headers;
  • ssl_context, for your company’s CA.

Proxies come from HTTPS_PROXY. Redirects are never followed, so the key can’t be sent elsewhere.

from tenon_auth import SecondStep, SignInContext, TenonAuthUnavailableError
@app.post("/login")
def login():
account = check_password(request.form["username"], request.form["password"]) # yours
try:
step = tenon_auth.begin_second_step(
str(account.id),
SignInContext(request.remote_addr, request.headers.get("User-Agent")), # ProxyFix for remote_addr
trusted_device_token=request.cookies.get("tenon_device"),
)
except TenonAuthUnavailableError:
return emergency_or_refuse(account) # fail closed
session["second_step"] = step.to_dict() # use server-side sessions (Flask-Session)
if step.kind == "CODE_ENTRY":
return redirect("/login/code")
if step.kind == "PUSH_WAIT":
return redirect("/login/approve")
if step.kind == "RECOVERY_ONLY":
return redirect("/login/recover")
return sign_in(account) # TRUSTED or NO_METHOD
@app.post("/login/code")
def code():
step = SecondStep.from_dict(session["second_step"])
result = tenon_auth.complete_with_code(step, request.form["code"])
if result.approved:
return sign_in_user(step.user_id)
if result.status == "WRONG_CODE":
return redirect(f"/login/code?left={result.attempts_remaining}")
return redirect("/login") # FAILED, EXPIRED

The other pages:

  • the waiting page calls tenon_auth.poll_approval(step) every 2 seconds;
  • the recovery page calls tenon_auth.complete_with_recovery_code(step, code).

Django: use the same calls in views, with request.session. In settings, configure the client once.

All errors are TenonAuthError subclasses:

  • TenonAuthUnavailableError (reason);
  • ApiKeyRejectedError, ForbiddenError;
  • InvalidRequestError, NotFoundError, InvalidCodeError;
  • FactorLockedError, TooManyRequestsError;
  • EmailRefusedError, EmailUnavailableError;
  • ConflictError.

See Errors.