Source code for govbr_auth.core.settings

"""Strict configuration for the new Gov.br OAuth core."""

from enum import StrEnum

from pydantic import (
    AnyHttpUrl,
    BaseModel,
    ConfigDict,
    NonNegativeInt,
    PositiveFloat,
    SecretStr,
    field_validator,
    model_validator,
)


[docs] class ProviderEnvironment(StrEnum): """Identify the Gov.br provider deployment used by the application.""" PRODUCTION = "production" STAGING = "staging" LOCAL = "local"
[docs] class GovBrSettings(BaseModel): """Validate immutable configuration for a Gov.br OAuth provider.""" model_config = ConfigDict( extra="forbid", frozen=True, hide_input_in_errors=True, str_strip_whitespace=True, ) environment: ProviderEnvironment = ProviderEnvironment.PRODUCTION authorization_url: AnyHttpUrl token_url: AnyHttpUrl userinfo_url: AnyHttpUrl client_id: str client_secret: SecretStr redirect_uri: AnyHttpUrl scope: str = "openid profile email" transaction_secret: SecretStr issuer: AnyHttpUrl jwks_url: AnyHttpUrl logout_url: AnyHttpUrl | None = None post_logout_redirect_uri: AnyHttpUrl | None = None connect_timeout_seconds: PositiveFloat = 5.0 read_timeout_seconds: PositiveFloat = 10.0 clock_skew_seconds: NonNegativeInt = 60
[docs] @field_validator("client_id", "scope") @classmethod def validate_nonempty_text(cls, value: str) -> str: """Reject empty values needed to create a valid OAuth request.""" if not value: raise ValueError("must not be empty") return value
[docs] @field_validator("client_secret", "transaction_secret") @classmethod def validate_nonempty_secret(cls, value: SecretStr) -> SecretStr: """Reject whitespace-only credentials without exposing their contents.""" if not value.get_secret_value().strip(): raise ValueError("must not be empty") return value
[docs] @model_validator(mode="after") def validate_url_schemes(self) -> "GovBrSettings": """Require HTTPS except for loopback-only configuration.""" for environment_name, url in self._configured_urls(): if url is None: continue if url.scheme == "https": continue host = (url.host or "").strip("[]") if host not in {"localhost", "127.0.0.1", "::1"}: if environment_name == "GOVBR_REDIRECT_URI": correction = ( "Configure HTTPS para esse DNS ou use uma URI de loopback" ) else: correction = "Configure HTTPS ou use um endpoint de loopback" raise ValueError( f"{environment_name} usa HTTP em um host não-loopback. " f"{correction}" ) return self
[docs] @model_validator(mode="after") def validate_logout_configuration(self) -> "GovBrSettings": """Require a complete, paired logout configuration.""" if (self.logout_url is None) != (self.post_logout_redirect_uri is None): raise ValueError( "logout_url and post_logout_redirect_uri must be configured together" ) return self
[docs] @model_validator(mode="after") def validate_official_provider_environment(self) -> "GovBrSettings": """Reject official Gov.br endpoints from another deployment.""" mismatched_variables = [ environment_name for environment_name, url in self._provider_urls() if url is not None and _OFFICIAL_GOVBR_HOST_ENVIRONMENTS.get(url.host.rstrip(".")) is not None and _OFFICIAL_GOVBR_HOST_ENVIRONMENTS[url.host.rstrip(".")] is not self.environment ] if mismatched_variables: rendered_variables = ", ".join(mismatched_variables) raise ValueError( "Endpoints oficiais do Gov.br incompatíveis com " f"GOVBR_ENVIRONMENT='{self.environment.value}': " f"{rendered_variables}" ) return self
def _configured_urls(self) -> tuple[tuple[str, AnyHttpUrl | None], ...]: """Return every OAuth URL with its public environment name.""" return ( ("GOVBR_AUTHORIZATION_URL", self.authorization_url), ("GOVBR_TOKEN_URL", self.token_url), ("GOVBR_USERINFO_URL", self.userinfo_url), ("GOVBR_REDIRECT_URI", self.redirect_uri), ("GOVBR_ISSUER", self.issuer), ("GOVBR_JWKS_URL", self.jwks_url), ("GOVBR_LOGOUT_URL", self.logout_url), ("GOVBR_POST_LOGOUT_REDIRECT_URI", self.post_logout_redirect_uri), ) def _provider_urls(self) -> tuple[tuple[str, AnyHttpUrl | None], ...]: """Return provider endpoints with their public environment names.""" return ( ("GOVBR_AUTHORIZATION_URL", self.authorization_url), ("GOVBR_TOKEN_URL", self.token_url), ("GOVBR_USERINFO_URL", self.userinfo_url), ("GOVBR_ISSUER", self.issuer), ("GOVBR_JWKS_URL", self.jwks_url), ("GOVBR_LOGOUT_URL", self.logout_url), )
_OFFICIAL_GOVBR_HOST_ENVIRONMENTS = { "sso.acesso.gov.br": ProviderEnvironment.PRODUCTION, "sso.staging.acesso.gov.br": ProviderEnvironment.STAGING, }