Auth Exceptions

Exceptions for the authentication module: OAuth2/OIDC providers, tokens, and session management.

Exception hierarchy

AuthError (base)
├── ConfigurationError        # Invalid auth configuration
├── ProviderNotFoundError     # Provider not in config
├── DiscoveryError            # OIDC discovery failed
├── TokenError
│   ├── TokenExpiredError     # Token has expired
│   ├── TokenRefreshError     # Refresh token failed
│   ├── TokenExchangeError    # Code exchange failed
│   ├── TokenValidationError  # Token validation failed
│   └── TokenStorageError     # Token storage operation failed
├── AuthorizationError        # Authorization flow failed
├── CallbackServerError       # OAuth callback server issue
└── PreflightError            # Preflight checks failed

Common failure modes

  • ProviderNotFoundError is raised when requesting a provider not defined in kstlib.conf.yml.

  • DiscoveryError surfaces when OIDC auto-discovery fails. Its status_code attribute tells whether the provider answered an HTTP error (int) or could not be reached at all (None).

  • TokenExpiredError indicates the access token has expired and refresh is needed.

  • TokenRefreshError is raised when refresh token exchange fails (revoked, expired refresh token). Its retryable attribute is True when retrying may succeed (transport failure or provider 5xx).

  • TokenExchangeError is raised when the authorization code exchange fails. Its status_code, error_code and retryable attributes encode where the exchange failed (see the pattern below).

  • CallbackServerError occurs when the local OAuth callback server cannot start (port in use).

Usage patterns

Distinguishing provider rejection from transport failure

A user-facing error page (or a log line) should not claim “the identity provider could not be reached” when the provider was reached and answered a 4xx. TokenExchangeError and DiscoveryError carry a structural discriminant so consumers never have to parse messages:

  • status_code is not None: the provider was reached and answered this HTTP error status. On exchange, error_code is then guaranteed non-None (the OAuth2 error code, or "unknown" when the body is unreadable).

  • status_code is None with error_code set (exchange only): a local pre-network guard rejected the exchange before any request was sent ("state_mismatch", "pkce_missing"). Restart the authorization flow.

  • Both None: transport failure, the provider was never reached.

from kstlib.auth.errors import TokenExchangeError

try:
    token = provider.exchange_code(code=code, state=state)
except TokenExchangeError as e:
    if e.status_code is not None:
        # The provider answered an error status (4xx/5xx).
        message = f"The identity provider rejected the login: {e.error_code}"
    elif e.error_code is not None:
        # Local guard, no request was sent: restart the flow.
        message = "The login flow must be restarted"
    else:
        # No HTTP response: DNS failure, connection refused, timeout.
        message = "The identity provider could not be reached"
    if e.retryable:
        message += " (temporary, retrying may succeed)"

The same discriminant applies to discovery:

from kstlib.auth.errors import DiscoveryError

try:
    provider.discover()
except DiscoveryError as e:
    if e.status_code is not None:
        logger.error("Provider answered HTTP %d to discovery", e.status_code)
    else:
        logger.error("Provider unreachable during discovery: %s", e.reason)

Handling token expiry

from kstlib.auth import AuthSession
from kstlib.auth.errors import TokenExpiredError, TokenRefreshError

session = AuthSession(provider="keycloak")

try:
    token = session.get_valid_token()
except TokenExpiredError:
    try:
        token = session.refresh()
    except TokenRefreshError:
        # Refresh failed - need full re-auth
        token = session.login()

Safe provider lookup

from kstlib.auth.config import get_provider_config
from kstlib.auth.errors import ProviderNotFoundError

try:
    config = get_provider_config("my-provider")
except ProviderNotFoundError:
    logger.error("Provider not configured")
    config = get_provider_config("default")

Handling callback server issues

from kstlib.auth import AuthSession
from kstlib.auth.errors import CallbackServerError

try:
    session.login()
except CallbackServerError as e:
    logger.error(f"Callback server failed: {e}")
    print("Try closing other applications using port 8400")

API reference

Authentication module exceptions.

exception kstlib.auth.errors.AuthError(message, *, details=None)[source]

Bases: KstlibError

Base exception for all authentication errors.

__init__(self, message: 'str', *, details: 'dict[str, Any] | None' = None) 'None' -> None[source]

Initialize the auth error with a message and optional structured details.

exception kstlib.auth.errors.AuthExpiredError(message, *, token_source=None, suggested_action=None)[source]

Bases: AuthError

Raised when an authenticated request returns HTTP 401 indicating token expiration.

Surfaced by kstlib.rapi.client (and any other consumer) when a server response signals that the previously-valid access token has expired or been invalidated during the session. The user must re-authenticate via the appropriate channel (for example, sas-admin auth login for Viya, or via a dedicated OAuth client when configured in kstlib.auth).

Note

Distinct from TokenExpiredError. The two cover different lifecycle points and originate from different sub-systems :

  • AuthExpiredError (this class, inherits from AuthError) is raised by kstlib.rapi.client when the server returns HTTP 401 at runtime, signalling that a token which was valid at send time has been expired or invalidated by the identity provider during the session.

  • TokenExpiredError (inherits from TokenError) is raised by kstlib.auth when a loaded token is detected as already expired before the request is sent (client-side pre-flight check).

token_source

Optional label identifying where the token was loaded from (for example, '~/.sas/credentials.json', 'env:KSTLIB_TOKEN', 'sops:secrets/api.sops.json'). None when the source is unknown.

suggested_action

Optional human-readable hint guiding the user toward a successful re-authentication (for example, 'Run: sas-admin auth login -u <user>'). None when no contextual hint is available.

Examples

>>> err = AuthExpiredError(
...     "Access token expired (HTTP 401).",
...     token_source="~/.sas/credentials.json",
...     suggested_action="Run: sas-admin auth login -u <user>",
... )
>>> err.token_source
'~/.sas/credentials.json'
>>> isinstance(err, AuthError)
True
__init__(self, message: 'str', *, token_source: 'str | None' = None, suggested_action: 'str | None' = None) 'None' -> None[source]

Initialize AuthExpiredError.

Parameters:
  • message (str) – Human-readable description of the expiration (typically including the HTTP status and a short rationale, never the raw token or response body).

  • token_source (str | None) – Optional label for where the token came from (used by callers to surface a contextual hint without exposing the secret material itself).

  • suggested_action (str | None) – Optional hint pointing the user to the right re-authentication procedure.

exception kstlib.auth.errors.AuthorizationError(reason, *, error_code=None, error_description=None)[source]

Bases: AuthError

Raised during authorization flow failures.

__init__(self, reason: 'str', *, error_code: 'str | None' = None, error_description: 'str | None' = None) 'None' -> None[source]

Initialize with the reason for the failure plus optional OAuth error code and description.

exception kstlib.auth.errors.CallbackServerError(reason, *, port=None)[source]

Bases: AuthError

Raised when the local callback server fails to start or receive callback.

__init__(self, reason: 'str', *, port: 'int | None' = None) 'None' -> None[source]

Initialize with the reason for the callback server failure and the port that was in use.

exception kstlib.auth.errors.ConfigurationError(message, *, details=None)[source]

Bases: AuthError

Raised when auth configuration is invalid or missing.

exception kstlib.auth.errors.DiscoveryError(issuer, reason, *, status_code=None)[source]

Bases: AuthError

Raised when OIDC discovery fails.

status_code encodes whether the provider answered the discovery request, so consumers can distinguish “the provider answered an error status” from “the provider could not be reached” without parsing reason:

  • status_code is not None: the provider was reached and answered this HTTP error status to the discovery request.

  • status_code is None: transport-level failure (DNS resolution, connection refused, timeout): no HTTP response was received.

issuer

Issuer URL whose discovery document could not be fetched.

reason

Human-readable description of the failure.

status_code

HTTP status answered by the provider, or None when no HTTP response was received.

Examples

>>> err = DiscoveryError("https://idp.example.com", "HTTP 502", status_code=502)
>>> err.status_code
502
>>> unreachable = DiscoveryError("https://idp.example.com", "connection refused")
>>> unreachable.status_code is None
True
__init__(self, issuer: 'str', reason: 'str', *, status_code: 'int | None' = None) 'None' -> None[source]

Initialize with the failing issuer URL, the failure reason and the HTTP status.

Parameters:
  • issuer (str) – Issuer URL whose discovery document could not be fetched.

  • reason (str) – Human-readable description of the failure.

  • status_code (int | None) – HTTP status answered by the provider. None (default) when no HTTP response was received.

exception kstlib.auth.errors.PreflightError(step, reason)[source]

Bases: AuthError

Raised when preflight validation fails.

__init__(self, step: 'str', reason: 'str') 'None' -> None[source]

Initialize with the failing preflight step name and the reason for the failure.

exception kstlib.auth.errors.ProviderNotFoundError(provider_name)[source]

Bases: AuthError

Raised when a named provider is not configured.

__init__(self, provider_name: 'str') 'None' -> None[source]

Initialize with the name of the missing provider.

exception kstlib.auth.errors.TokenError(message, *, details=None)[source]

Bases: AuthError

Base exception for token-related errors.

exception kstlib.auth.errors.TokenExchangeError(reason, *, error_code=None, status_code=None, retryable=False)[source]

Bases: TokenError

Raised when authorization code exchange fails.

The attributes encode where the exchange failed, so consumers can distinguish “the provider answered an error” from “the provider could not be reached” without parsing messages:

  • status_code is not None: the provider was reached and answered this HTTP error status. error_code is then guaranteed non-None: the OAuth2 error code from the response body, or "unknown" when the body is missing or unreadable.

  • status_code is None and error_code is None: transport-level failure (DNS resolution, connection refused, timeout): no HTTP response was received from the provider.

  • status_code is None and error_code is not None: a local pre-network guard rejected the exchange before any request was sent. Guard codes: "state_mismatch" (CSRF state validation failed) and "pkce_missing" (PKCE enabled but no code verifier available).

reason

Human-readable description of the exchange failure.

error_code

OAuth2 error code answered by the provider ("unknown" when the error body is unreadable), one of the local guard codes listed above, or None for transport-level failures.

status_code

HTTP status answered by the provider, or None when no HTTP response was received (transport failure or local guard).

retryable

True when retrying the same exchange may succeed (transport failure or provider 5xx answer). False when the rejection is definitive: provider 4xx (authorization codes are single-use) or local guard (restart the authorization flow instead). Same semantics as TokenRefreshError.retryable.

Examples

>>> rejected = TokenExchangeError("Rejected", error_code="not_allowed", status_code=400)
>>> rejected.status_code is not None  # the provider answered
True
>>> rejected.retryable
False
>>> transport = TokenExchangeError("Network error: timeout", retryable=True)
>>> transport.status_code is None and transport.error_code is None
True
__init__(self, reason: 'str', *, error_code: 'str | None' = None, status_code: 'int | None' = None, retryable: 'bool' = False) 'None' -> None[source]

Initialize with the exchange failure reason and its structured discriminants.

Parameters:
  • reason (str) – Human-readable description of the exchange failure.

  • error_code (str | None) – OAuth2 error code from the provider response body, or a local guard code. None (default) for transport failures.

  • status_code (int | None) – HTTP status answered by the provider. None (default) when no HTTP response was received.

  • retryable (bool) – Whether retrying the same exchange may succeed.

exception kstlib.auth.errors.TokenExpiredError(message, *, details=None)[source]

Bases: TokenError

Raised when a token has expired and cannot be refreshed.

exception kstlib.auth.errors.TokenRefreshError(reason, *, retryable=False)[source]

Bases: TokenError

Raised when token refresh fails.

reason

Human-readable description of the refresh failure.

retryable

True when retrying the refresh may succeed (transport failure or provider 5xx answer). False when the rejection is definitive (provider 4xx: invalid, expired or revoked refresh token, or a misconfigured token endpoint). Same semantics as TokenExchangeError.retryable.

__init__(self, reason: 'str', *, retryable: 'bool' = False) 'None' -> None[source]

Initialize with the reason for the refresh failure and a retryable flag.

exception kstlib.auth.errors.TokenStorageError(message, *, details=None)[source]

Bases: TokenError

Raised when token persistence fails (save/load/delete).

exception kstlib.auth.errors.TokenValidationError(reason, *, claim=None)[source]

Bases: TokenError

Raised when JWT validation fails (signature, claims, expiry).

__init__(self, reason: 'str', *, claim: 'str | None' = None) 'None' -> None[source]

Initialize with the reason for the validation failure and the offending claim name.