Source code for indieweb_utils.indieauth.flask

from dataclasses import dataclass
from typing import List

import requests


[docs]@dataclass class IndieAuthCallbackResponse: message: str response: dict
class AuthenticationError(Exception): pass def _validate_indieauth_response(me: str, response: requests.Response, required_scopes: List[str]) -> None: if me is None: message = "An invalid me value was provided." raise AuthenticationError(message) response_data = response.json() if response_data.get("me") is None: raise AuthenticationError("There was an error with the IndieAuth server.") if response_data.get("me").strip("/") != me.strip("/"): message = "Your domain is not allowed to access this website." raise AuthenticationError(message) granted_scopes = response_data.get("scope").split(" ") if response_data.get("scope") is None or any(scope not in granted_scopes for scope in required_scopes): message = f"You need to grant {', '.join(required_scopes).strip(', ')} access to use this tool." raise AuthenticationError(message)
[docs]def indieauth_callback_handler( *, code: str, state: str, token_endpoint: str, code_verifier: str, session_state: str, me: str, callback_url: str, client_id: str, required_scopes: List[str], ) -> IndieAuthCallbackResponse: """ Exchange a callback 'code' for an authentication token. :param code: The callback 'code' to exchange for an authentication token. :type code: str :param state: The state provided by the authentication server in the callback response. :type state: str :param token_endpoint: The token endpoint to use for exchanging the callback 'code' for an authentication token. :type token_endpoint: str :param code_verifier: The code verifier to use for exchanging the callback 'code' for an authentication token. :type code_verifier: str :param session_state: The state stored in session used to verify the callback state is valid. :type session_state: str :param me: The URL of the user's profile. :type me: str :param callback_url: The callback URL used in the original authentication request. :type callback_url: str :param client_id: The client ID used in the original authentication request. :type client_id: str :param required_scopes: The scopes required for the application to work. This list should not include optional scopes. :type required_scopes: list[str] :return: A message indicating the result of the callback (success or failure) and the token endpoint response. The endpoint response will be equal to None if the callback failed. :rtype: tuple[str, dict] Example: .. code-block:: python import indieweb_utils from Flask import flask app = Flask(__name__) @app.route("/indieauth/callback") def callback(): response = indieweb_utils.indieauth_callback_handler( code=request.args.get("code"), state=request.args.get("state"), token_endpoint="https://tokens.indieauth.com/token", code_verifier=session["code_verifier"], session_state=session["state"], me=session["me"], callback_url=session["callback_url"], client_id=session["client_id"], required_scopes=["create", "update", "delete"], ) return response.message :raises AuthenticationError: The token endpoint could not be accessed or authentication failed. """ if state != session_state: message = "The provided state value did not match the session state. Please try again." raise AuthenticationError(message) data = { "code": code, "redirect_uri": callback_url, "client_id": client_id, "grant_type": "authorization_code", "code_verifier": code_verifier, } headers = {"Accept": "application/json"} try: auth_request = requests.post(token_endpoint, data=data, headers=headers) except requests.exceptions.RequestException: message = "Your token endpoint server could not be accessed." raise AuthenticationError(message) if auth_request.status_code != 200: message = "There was an error with your token endpoint server." raise AuthenticationError(message) # remove code verifier from session because the authentication flow has finished _validate_indieauth_response(me, auth_request, required_scopes) return IndieAuthCallbackResponse(message="Authentication was successful.", response=auth_request.json())
[docs]def is_authenticated(token_endpoint: str, headers: dict, session: dict, approved_user: bool = None) -> bool: """ Check if a user has provided a valid Authorization header or access token in session. Designed for use with Flask. :param token_endpoint: The token endpoint of the user's IndieAuth server. :param headers: The headers sent by a request. :param session: The session object from a Flask application. :param approved_user: The optional URL of the that is approved to use the API. :return: True if the user is authenticated, False otherwise. :rtype: bool Example: .. code-block:: python import indieweb_utils from Flask import flask, request app = Flask(__name__) @app.route("/") def index(): user_is_authenticated = indieweb_utils.is_authenticated( "https://tokens.indieauth.com/token", request.headers, session, "https://example.com/", ) if user_is_authenticated is False: return "Not authenticated" return "Authenticated" :raises AuthenticationError: The token endpoint could not be accessed. """ if headers.get("Authorization") is not None: access_token = headers["Authorization"].split(" ")[-1] elif session.get("access_token"): access_token = session.get("access_token") else: return False try: check_token = requests.get(token_endpoint, headers={"Authorization": f"Bearer {access_token}"}, timeout=5) except requests.exceptions.Timeout: raise AuthenticationError("The specified token endpoint timed out.") except requests.exceptions.RequestException: raise AuthenticationError("The specified token endpoint could not be accessed.") if check_token.status_code != 200 or not check_token.json().get("me"): return False if approved_user is not None and check_token.status_code != 200 and check_token.json()["me"] != approved_user: return False return True