Source code for plone_client.auth

"""Authentication strategies for :class:`~plone_client.PloneClient`.

Three ways to authenticate against ``plone.restapi`` are supported:

* HTTP basic authentication (:class:`httpx.BasicAuth`), the simplest option
  and the default when a username and password are given;
* a JWT obtained from ``@login`` (:class:`JWTAuth`), which logs in again
  when the token expires;
* a token issued beforehand (:class:`TokenAuth`).

Any other :class:`httpx.Auth` can be passed to the client as well.
"""

from .exceptions import error_from_response
from collections.abc import Generator
from typing import Literal

import httpx


AuthMethod = Literal["basic", "jwt"]


[docs] class TokenAuth(httpx.Auth): """Send a token issued beforehand as ``Authorization: Bearer``. :param token: The JWT. """ def __init__(self, token: str) -> None: self.token = token
[docs] def auth_flow( self, request: httpx.Request ) -> Generator[httpx.Request, httpx.Response, None]: request.headers["Authorization"] = f"Bearer {self.token}" yield request
[docs] class JWTAuth(httpx.Auth): """Log in with ``POST @login`` and send the JWT it returns. The login happens on the first request. When the site answers ``401``, the token is assumed to be expired: the flow logs in again and repeats the request once. :param username: Login name. :param password: Password. :param login_url: Absolute URL of the site's ``@login`` service. """ requires_response_body = True def __init__(self, username: str, password: str, login_url: str) -> None: self.username = username self.password = password self.login_url = login_url self.token = "" def _login(self) -> Generator[httpx.Request, httpx.Response, None]: """Ask the site for a new token. :raises PloneAuthenticationError: If the site refuses the credentials. """ response = yield httpx.Request( "POST", self.login_url, json={"login": self.username, "password": self.password}, headers={"Accept": "application/json"}, ) if response.status_code != 200: raise error_from_response(response) self.token = response.json().get("token", "")
[docs] def auth_flow( self, request: httpx.Request ) -> Generator[httpx.Request, httpx.Response, None]: if not self.token: yield from self._login() request.headers["Authorization"] = f"Bearer {self.token}" response = yield request if response.status_code == 401: yield from self._login() request.headers["Authorization"] = f"Bearer {self.token}" yield request
[docs] def build_auth( *, username: str = "", password: str = "", token: str = "", method: AuthMethod = "basic", login_url: str = "", ) -> httpx.Auth | None: """Pick the authentication for a set of credentials. A token wins over a username and password. Without any credential the client is anonymous. :param username: Login name. :param password: Password. :param token: JWT issued beforehand. :param method: ``basic`` or ``jwt``, used with a username and password. :param login_url: Absolute URL of ``@login``, needed by ``jwt``. :returns: The authentication, or ``None`` for anonymous access. :raises ValueError: If the method is unknown or only half of the username/password pair is given. """ if token: return TokenAuth(token) if not (username or password): return None if not (username and password): raise ValueError("Both a username and a password are needed") if method == "basic": return httpx.BasicAuth(username, password) if method == "jwt": return JWTAuth(username, password, login_url) raise ValueError(f"Unknown authentication method: {method!r}")