From 062364fefad4809aced3eae400a3253c4caea5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 2 Oct 2026 17:11:10 +0200 Subject: [PATCH] docs(auth): document structured failures and exception migration Help integrations choose recovery from explicit metadata and migrate removed specialized exceptions. Correct backend examples while keeping the docs build independent of the unreleased packages. --- docs/backends/azuread.rst | 3 +- docs/backends/email.rst | 12 +- docs/backends/implementation.rst | 14 +- docs/backends/saml.rst | 2 +- docs/backends/username.rst | 12 +- docs/configuration/django.rst | 30 ++- docs/configuration/settings.rst | 8 +- docs/exceptions.rst | 313 +++++++++++++++++++++++++++---- 8 files changed, 343 insertions(+), 51 deletions(-) diff --git a/docs/backends/azuread.rst b/docs/backends/azuread.rst index c1f95c6f..e40dae42 100644 --- a/docs/backends/azuread.rst +++ b/docs/backends/azuread.rst @@ -421,7 +421,8 @@ a Django view can retrieve the token before clearing the local session: Use a CSRF-protected POST form to invoke this view. This example assumes an authenticated B2C user and a single configured sign-in policy. Applications with multiple policies must select the backend for the stored sign-in policy. -A missing or invalid ``end_session_endpoint`` raises ``AuthMissingParameter``; +A missing or invalid ``end_session_endpoint`` raises ``AuthResponseError`` +with ``code="missing_claim"`` or ``code="invalid_claim"``, respectively; discovery request failures propagate through the usual backend error handling. Provider logout complements local logout. Disconnecting an account removes diff --git a/docs/backends/email.rst b/docs/backends/email.rst index 851545ef..85916982 100644 --- a/docs/backends/email.rst +++ b/docs/backends/email.rst @@ -56,6 +56,9 @@ Password handling Here's an example of password handling to add to the pipeline:: + from social_core.exceptions import AuthCredentialError + + def user_password(strategy, backend, user, is_new=False, *args, **kwargs): if backend.name != 'email': return @@ -66,7 +69,14 @@ Here's an example of password handling to add to the pipeline:: user.save() elif not user.validate_password(password): # return {'user': None, 'social': None} - raise AuthForbidden(backend) + raise AuthCredentialError( + backend, + code="credential_rejected", + source="request", + stage="pipeline", + parameter="password", + recovery="correct_input", + ) .. _python-social-auth: https://github.com/python-social-auth .. _EmailAuth: https://github.com/python-social-auth/social-core/blob/master/social_core/backends/email.py diff --git a/docs/backends/implementation.rst b/docs/backends/implementation.rst index 106dcc66..bd81e3bb 100644 --- a/docs/backends/implementation.rst +++ b/docs/backends/implementation.rst @@ -266,7 +266,7 @@ redirected. Example code:: from social_core.backends.open_id import OpenIdAuth - from social_core.exceptions import AuthMissingParameter + from social_core.exceptions import AuthInputError class LiveJournalOpenId(OpenIdAuth): @@ -284,7 +284,7 @@ Example code:: def openid_url(self): """Returns LiveJournal authentication URL""" if not self.data.get('openid_lj_user'): - raise AuthMissingParameter(self, 'openid_lj_user') + raise AuthInputError(self, code="missing_parameter", parameter="openid_lj_user", stage="begin") return 'http://%s.livejournal.com' % self.data['openid_lj_user'] @@ -300,7 +300,7 @@ Example code:: from google.appengine.api import users from social_core.backends.base import BaseAuth - from social_core.exceptions import AuthException + from social_core.exceptions import AuthUnknownError class GoogleAppEngineAuth(BaseAuth): @@ -329,7 +329,7 @@ Example code:: def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance.""" if not users.get_current_user(): - raise AuthException('Authentication error') + raise AuthUnknownError(self, code="unknown_error", stage="callback") kwargs.update({'response': '', 'backend': self}) return self.strategy.authenticate(*args, **kwargs) @@ -340,6 +340,12 @@ Common backend methods All backends inherit from ``BaseAuth`` which provides several methods that can be overridden to customize behavior. Here are some key methods: +``process_error(data, *, stage="callback")`` + Detects provider errors in callbacks and successful HTTP responses. OAuth2 + backends also call this hook during token exchange and refresh. Overrides + must accept the keyword-only ``stage`` argument, pass it to the superclass, + and use it when constructing structured exceptions. See :doc:`../exceptions`. + ``id_key()`` Returns the ID key to use for this backend. By default, this method checks if the ``ID_KEY`` has been configured via settings (using diff --git a/docs/backends/saml.rst b/docs/backends/saml.rst index b85d3216..18900864 100644 --- a/docs/backends/saml.rst +++ b/docs/backends/saml.rst @@ -292,7 +292,7 @@ particular, there are two methods that are designed for subclasses to override: on the user's SAML attributes. For example, you can restrict access to your application to only accept users who belong to a certain department. After inspecting the passed attributes parameter, do nothing to allow the user to - login, or raise ``social_core.exceptions.AuthForbidden`` to reject the user. + login, or raise ``social_core.exceptions.AuthPolicyError`` to reject the user. Troubleshooting diff --git a/docs/backends/username.rst b/docs/backends/username.rst index 5dd542bb..085d7375 100644 --- a/docs/backends/username.rst +++ b/docs/backends/username.rst @@ -51,6 +51,9 @@ Password handling Here's an example of password handling to add to the pipeline:: + from social_core.exceptions import AuthCredentialError + + def user_password(strategy, user, is_new=False, *args, **kwargs): if strategy.backend.name != 'username': return @@ -61,7 +64,14 @@ Here's an example of password handling to add to the pipeline:: user.save() elif not user.validate_password(password): # return {'user': None, 'social': None} - raise AuthException(strategy.backend) + raise AuthCredentialError( + strategy.backend, + code="credential_rejected", + source="request", + stage="pipeline", + parameter="password", + recovery="correct_input", + ) .. _python-social-auth: https://github.com/python-social-auth .. _UsernameAuth: https://github.com/python-social-auth/social-core/blob/master/social_core/backends/username.py diff --git a/docs/configuration/django.rst b/docs/configuration/django.rst index 9fb74803..4f8de2cf 100644 --- a/docs/configuration/django.rst +++ b/docs/configuration/django.rst @@ -496,8 +496,8 @@ When query parameter transport is active (or triggered via fallback), the redire destination receives two query parameters: ``message = ''`` - Message from the exception raised. In some cases, this is the error message - returned by the provider during the authentication process. + Safe default message from the exception raised. Provider descriptions are + retained in the exception's diagnostic ``detail`` and are not sent to clients. ``backend = ''`` Backend name that was used, or ``unknown-backend`` if unresolved. @@ -582,6 +582,32 @@ Exception processing is disabled if any of these settings is defined with a DEBUG = True +Structured authentication errors +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`authentication-exceptions` for the exception families and recovery +contract. The middleware uses safe default messages; provider diagnostics and +identifying context are not included in client transports. + +Enable metadata in query transport (including message-storage fallback) with: + +.. code-block:: python + + SOCIAL_AUTH_ERROR_INCLUDE_METADATA = True # Default: False + +The redirect receives ``error_code``, ``error_source``, ``error_stage``, and +``error_recovery`` alongside the configured message/backend parameters. +Backend-specific settings are supported. Existing query parameters and fragments +are preserved; stale metadata values are replaced when metadata is enabled. +If a configured message/backend parameter name matches a metadata key, the +configured parameter takes precedence and that metadata field is omitted. + +Applications should select their own messages and redirects from stable codes, +and should decide reporting independently of suggested recovery actions. +Subclasses overriding ``dispatch_error`` or ``append_query_params`` must accept +the new optional ``metadata`` argument. + + Launch Bridge Endpoints ----------------------- diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index ee2dfdc6..13418fa2 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -259,8 +259,8 @@ An explicitly configured ``ID_KEY`` takes precedence over older backend-specific identifier selectors such as ``USERNAME_AS_ID``, ``USE_UNIQUE_USER_ID``, and ``IDENTIFIED_BY_PERMANENT_ID``. An explicitly configured field must be present and non-empty in the provider -data; otherwise authentication fails with ``AuthMissingParameter`` rather -than storing an ambiguous user identifier. +data; otherwise authentication fails with ``AuthResponseError`` and +``code="missing_claim"`` rather than storing an ambiguous user identifier. Example: Configure Seznam backend to use ``id`` instead of the default ``oauth_user_id``:: @@ -382,11 +382,11 @@ address or domain name. To white-list just set any of these settings: ``SOCIAL_AUTH__WHITELISTED_DOMAINS = ['foo.com', 'bar.com']`` Supply a list of domain names to be white-listed. Any user with an email address on any of the allowed domains will login successfully, otherwise - ``AuthForbidden`` is raised. + ``AuthPolicyError`` is raised. ``SOCIAL_AUTH__WHITELISTED_EMAILS = ['me@foo.com', 'you@bar.com']`` Supply a list of email addresses to be white-listed. Any user with an email - address in this list will login successfully, otherwise ``AuthForbidden`` + address in this list will login successfully, otherwise ``AuthPolicyError`` is raised. diff --git a/docs/exceptions.rst b/docs/exceptions.rst index a7d22829..59d02f89 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -1,55 +1,294 @@ +.. _authentication-exceptions: + Exceptions ========== -This set of exceptions were introduced to describe the situations a bit more -than just the ``ValueError`` usually raised. - -``SocialAuthBaseException`` - Base class for all social auth exceptions. +Social Auth exposes structured exceptions so applications can choose recovery +without matching provider descriptions or exception messages. -``AuthException`` - Base exception class for authentication process errors. +Catch ``SocialAuthBaseException`` for all Social Auth failures, including +configuration errors. Catch ``AuthException`` for authentication-flow failures. +Both retain their existing inheritance, including ``ValueError``. Configuration +errors inherit directly from ``SocialAuthBaseException``. -``AuthFailed`` - Authentication failed for some reason. +Exception families +------------------ +``AuthConfigurationError`` + Missing or invalid settings, unavailable backends, or unsupported features. +``AuthInputError`` + Missing or invalid request or application input. +``AuthSessionError`` + Missing authentication context, state mismatch, or a different initiating user. +``AuthResponseError`` + Malformed provider responses or failed signature, claim, nonce, or expiry validation. +``AuthCredentialError`` + Rejected credentials, rejected authorization codes, revoked tokens, or required reauthentication. +``AuthPolicyError`` + Application authentication, membership, or disconnect policy rejection. +``AuthAssociationError`` + Local account conflicts or unsafe identifier migration. +``AuthProviderError`` + Connection, timeout, TLS, rate-limit, availability, or HTTP failures. ``AuthCanceled`` - Authentication was canceled by the user. - + Explicit authorization cancellation or refusal. ``AuthUnknownError`` - An unknown error stopped the authentication process. + Authentication failures without a known classification. + +Structured attributes +--------------------- + +Each exception exposes ``code``, ``source``, ``stage``, and ``recovery``. Codes +are stable machine-readable strings; messages and diagnostic descriptions are +not part of the classification contract. + +``source`` identifies the failing boundary, not who is responsible: +``configuration``, ``request``, ``session``, ``provider_response``, +``local_policy``, ``storage``, or ``unknown``. + +``stage`` identifies the operation: ``begin``, ``callback``, ``token_exchange``, +``token_validation``, ``user_info``, ``pipeline``, ``refresh``, ``disconnect``, +or ``unknown``. Custom integrations should supply the stage at the raise site. + +``recovery`` suggests an action: ``none``, ``correct_input``, ``restart_login``, +``reauthenticate``, ``retry_later``, ``check_provider_profile``, +``use_existing_account``, or ``contact_administrator``. These hints do not +perform retries or redirects and do not determine whether to report a failure. + +Optional attributes are ``backend``, ``parameter``, ``claim``, ``provider_code``, +``status_code``, and ``retry_after``. ``retry_after`` preserves the provider's +HTTP header; applications must interpret it before using it. + +``str(exception)`` and ``exception.args`` contain a safe default message. +Provider descriptions are available separately in ``detail``. ``context`` is +an explicitly supplied mapping for diagnostic identifiers, such as user ID and +provider UID. Original exceptions remain available through exception chaining. +Do not send diagnostics, raw responses, or identifying context to client URLs +or flash messages. Do not log tokens, cookies, or full authentication assertions. + +``public_metadata()`` returns only ``error_code``, ``error_source``, +``error_stage``, and ``error_recovery``. + +.. code-block:: python + + from social_core.exceptions import AuthResponseError, AuthException + + if "sub" not in claims: + raise AuthResponseError( + backend, code="missing_claim", claim="sub", stage="token_validation" + ) + + try: + authenticate() + except AuthException as error: + if error.code == "response_expired": + show_restart_login_message() + else: + show_generic_authentication_message() + +Application-specific codes should have a namespace, for example +``myapp.registration_disabled``. Explicitly set their source and recovery. +Unknown codes use the family's safe default message and metadata. Never derive +a code from a free-form message. + +Reason codes +------------ + +Defaults are listed below. A raise site can override source or recovery when +its operation supplies more precise information. + +.. list-table:: + :header-rows: 1 + + * - Code + - Source + - Suggested recovery + * - ``missing_setting`` + - ``configuration`` + - ``contact_administrator`` + * - ``invalid_setting`` + - ``configuration`` + - ``contact_administrator`` + * - ``unsupported_feature`` + - ``configuration`` + - ``contact_administrator`` + * - ``backend_missing`` + - ``configuration`` + - ``contact_administrator`` + * - ``missing_parameter`` + - ``request`` + - ``correct_input`` + * - ``invalid_parameter`` + - ``request`` + - ``correct_input`` + * - ``session_context_missing`` + - ``session`` + - ``restart_login`` + * - ``state_mismatch`` + - ``session`` + - ``restart_login`` + * - ``user_mismatch`` + - ``session`` + - ``restart_login`` + * - ``malformed_response`` + - ``provider_response`` + - ``contact_administrator`` + * - ``missing_claim`` + - ``provider_response`` + - ``contact_administrator`` + * - ``invalid_claim`` + - ``provider_response`` + - ``contact_administrator`` + * - ``invalid_signature`` + - ``provider_response`` + - ``contact_administrator`` + * - ``nonce_mismatch`` + - ``provider_response`` + - ``restart_login`` + * - ``response_expired`` + - ``provider_response`` + - ``restart_login`` + * - ``response_not_yet_valid`` + - ``provider_response`` + - ``contact_administrator`` + * - ``invalid_expiry`` + - ``storage`` + - ``contact_administrator`` + * - ``profile_email_missing`` + - ``provider_response`` + - ``check_provider_profile`` + * - ``authorization_code_rejected`` + - ``provider_response`` + - ``restart_login`` + * - ``credential_rejected`` + - ``provider_response`` + - ``reauthenticate`` + * - ``token_revoked`` + - ``provider_response`` + - ``reauthenticate`` + * - ``reauthentication_required`` + - ``storage`` + - ``reauthenticate`` + * - ``email_verification_rejected`` + - ``request`` + - ``restart_login`` + * - ``authentication_disallowed`` + - ``local_policy`` + - ``contact_administrator`` + * - ``membership_required`` + - ``local_policy`` + - ``contact_administrator`` + * - ``disconnect_disallowed`` + - ``local_policy`` + - ``none`` + * - ``identity_in_use`` + - ``storage`` + - ``use_existing_account`` + * - ``email_in_use`` + - ``storage`` + - ``use_existing_account`` + * - ``username_in_use`` + - ``storage`` + - ``use_existing_account`` + * - ``identifier_migration_conflict`` + - ``storage`` + - ``contact_administrator`` + * - ``connection_failed`` + - ``provider_response`` + - ``retry_later`` + * - ``timeout`` + - ``provider_response`` + - ``retry_later`` + * - ``tls_error`` + - ``provider_response`` + - ``contact_administrator`` + * - ``rate_limited`` + - ``provider_response`` + - ``retry_later`` + * - ``unavailable`` + - ``provider_response`` + - ``retry_later`` + * - ``http_error`` + - ``provider_response`` + - ``contact_administrator`` + * - ``authorization_declined`` + - ``provider_response`` + - ``none`` + * - ``unknown_error`` + - ``unknown`` + - ``contact_administrator`` + +Provider failures +----------------- -``AuthTokenError`` - Unauthorized or access token error, it was invalid, impossible to - authenticate or user removed permissions to it. +HTTP status alone does not establish cancellation, expired credentials, or a +local policy rejection. Shared HTTP handling retains status and structured +provider codes. Unknown provider codes remain provider errors. -``AuthMissingParameter`` - A needed parameter to continue the process was missing, usually raised by - the services that need some POST data like myOpenID. +For OAuth, ``invalid_client`` is a configuration failure and ``invalid_grant`` +is credential rejection. The latter does not establish expiry. Explicit +``access_denied`` indicates authorization refusal. HTTP 429 and server errors +receive retry-later guidance; TLS verification failures require administrator +attention without suggesting that verification be disabled. -``AuthAlreadyAssociated`` - A different user has already associated the social account that the current - user is trying to associate. +Migration from legacy exceptions +-------------------------------- -``WrongBackend`` - Raised when the backend given in the URLs is invalid (not enabled or - registered). +This is a breaking change. ``SocialAuthBaseException`` and ``AuthException`` +remain available for broad catches. ``AuthCanceled`` and ``AuthUnknownError`` +also remain available, so catches of these types can be retained. Removed names +have no aliases or wrappers. Update custom backends, pipelines, and catches of +removed types together with the library upgrade. -``NotAllowedToDisconnect`` - Raised on disconnect action when it's not safe for the user to disconnect - the social account, probably because the user lacks a password or another - social account. +.. list-table:: + :header-rows: 1 -``AuthStateMissing`` - The state parameter is missing from the server response. + * - Previous exception + - Replacement + * - ``AuthFailed`` / ``AuthTokenError`` + - Choose response, credential, session, policy, or provider failure from the actual cause. + * - ``AuthMissingParameter`` / ``AuthInvalidParameter`` + - Input errors for request data; configuration errors for settings; response errors for provider fields. + * - ``AuthStateMissing`` / ``AuthStateForbidden`` + - Session errors with ``session_context_missing`` / ``state_mismatch``. + * - ``AuthUserMismatch`` + - ``AuthSessionError`` with ``user_mismatch``. + * - ``AuthTooManyRequests`` + - ``AuthProviderError`` with ``rate_limited``. + * - ``AuthForbidden`` + - Local policy errors; session errors for user mismatch; provider errors for HTTP rejection. + * - ``AuthAlreadyAssociated`` + - Association errors with an explicit identity, username, email, or migration-conflict code. + * - ``AuthTokenRevoked`` / ``AuthReauthenticationRequired`` + - Credential errors with ``token_revoked`` / ``reauthentication_required``. + * - ``AuthConnectionError`` / ``AuthUnreachableProvider`` + - Provider errors distinguishing connection, timeout, TLS, rate limit, and availability. + * - ``InvalidEmail`` + - Credential error with ``email_verification_rejected``. + * - ``NotAllowedToDisconnect`` + - Policy error with ``disconnect_disallowed``. + * - ``InvalidExpiryValue`` + - Response error with ``invalid_expiry``, source ``storage``, and ``parameter`` identifying the field. + * - ``WrongBackend`` / ``MissingBackend`` + - Configuration error with ``backend_missing``. + * - Strategy/configuration errors / ``AuthNotImplementedParameter`` + - Configuration errors with missing/invalid settings or ``unsupported_feature``. -``AuthStateForbidden`` - The state parameter returned by the server is not the one sent. +Previously, ``AuthStateMissing`` meant missing session state, while missing +callback state raised ``AuthMissingParameter``. Preserve that distinction when +migrating: use a session error for missing saved state and an input error for a +missing callback parameter. -``AuthTokenRevoked`` - Raised when the user revoked the access_token in the provider. +Construct failures with a backend (or ``None`` where unavailable) and keyword +metadata. Keep provider descriptions in diagnostic positional arguments. +For example, replace ``AuthTokenError(backend, "Signature has expired")`` at a +confirmed expiry boundary with:: -``AuthUnreachableProvider`` - Raised when server couldn't communicate with backend. + AuthResponseError( + backend, + "Signature has expired", + code="response_expired", + stage="token_validation", + ) -These are a subclass of ``ValueError`` to keep backward compatibility. +Do not translate that text into a code elsewhere in the application.