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.