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.
Install
Section titled “Install”Download the wheel from Downloads, check its SHA-256, and install it:
pip install tenon_auth-1.0.0-py3-none-any.whlConfigure
Section titled “Configure”import osfrom 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.
Sign in (Flask)
Section titled “Sign in (Flask)”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, EXPIREDThe 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.
Errors
Section titled “Errors”All errors are TenonAuthError subclasses:
TenonAuthUnavailableError(reason);ApiKeyRejectedError,ForbiddenError;InvalidRequestError,NotFoundError,InvalidCodeError;FactorLockedError,TooManyRequestsError;EmailRefusedError,EmailUnavailableError;ConflictError.
See Errors.