from pydantic import model_validator
from canaille.app.configuration import BaseModel
from canaille.app.configuration import CommaSeparatedList
[docs]
class UserInfoMappingSettings(BaseModel):
"""Mapping between the user model and the JWT fields.
Fields are evaluated with jinja.
A ``user`` var is available.
"""
SUB: str | None = "{{ user.user_name }}"
NAME: str | None = (
"{% if user.formatted_name %}{{ user.formatted_name }}{% endif %}"
)
PHONE_NUMBER: str | None = (
"{% if user.phone_numbers %}{{ user.phone_numbers[0] }}{% endif %}"
)
EMAIL: str | None = (
"{% if user.preferred_email %}{{ user.preferred_email }}{% endif %}"
)
GIVEN_NAME: str | None = "{% if user.given_name %}{{ user.given_name }}{% endif %}"
FAMILY_NAME: str | None = (
"{% if user.family_name %}{{ user.family_name }}{% endif %}"
)
PREFERRED_USERNAME: str | None = (
"{% if user.display_name %}{{ user.display_name }}{% endif %}"
)
LOCALE: str | None = (
"{% if user.preferred_language %}{{ user.preferred_language }}{% endif %}"
)
ADDRESS: str | None = (
"{% if user.formatted_address %}{{ user.formatted_address }}{% endif %}"
)
PICTURE: str | None = (
"{% if user.photo %}{{ user | photo_url(external=True) }}{% endif %}"
)
WEBSITE: str | None = "{% if user.profile_url %}{{ user.profile_url }}{% endif %}"
UPDATED_AT: str | None = (
"{% if user.last_modified %}{{ user.last_modified.timestamp() }}{% endif %}"
)
[docs]
class OIDCSettings(BaseModel):
"""OpenID Connect settings.
Belong in the ``CANAILLE_OIDC`` namespace.
"""
ENABLE_OIDC: bool = True
"""Whether the Single Sign-On feature and the OpenID Connect API is enabled."""
DYNAMIC_CLIENT_REGISTRATION_OPEN: bool = False
"""Whether a token is needed for the RFC7591 dynamical client registration.
If :py:data:`True`, no token is needed to register a client.
If :py:data:`False`, dynamical client registration requires a valid JWT token
generated by the ``canaille oidc registration generate-token`` command.
"""
REQUIRE_NONCE: bool = True
"""Force the nonce exchange during the authentication flows.
This adds security but may not be supported by all clients.
"""
ACTIVE_JWKS: list[dict[str, str] | str] | None = None
"""The active JSON Web Keys Set.
Those keys are used to sign and verify JWTs.
The keys can be in the form of JWK dict or raw keys."""
INACTIVE_JWKS: list[dict[str, str] | str] | None = None
"""The inactive JSON Web Keys Set.
Those keys are only used to verify JWTs.
The keys can be in the form of JWK dict or raw keys."""
TRUSTED_DOMAINS: CommaSeparatedList = [".localhost", "127.0.0.1"]
"""Trusted domains for automatic client trust.
Clients with a :attr:`~canaille.oidc.basemodels.Client.client_uri` matching
these domains will be automatically considered as trusted and will not
display the consent page to users.
This is particularly useful for development environments.
Supports these patterns:
- Exact match: ``example.com`` matches ``example.com``
- Wildcard match: ``.example.com`` matches ``example.com`` and all its subdomains
Examples:
- ``[".localhost", "127.0.0.1"]`` (default for development)
- ``[".dev.company.com", "staging.company.com"]``
- ``[".local", "localhost"]``
"""
USERINFO_MAPPING: UserInfoMappingSettings | None = UserInfoMappingSettings()
""""Attribute mapping used to build an OIDC UserInfo object.
UserInfo is used to fill the id_token and the userinfo endpoint."""
@model_validator(mode="after")
def validate_rsa_key_present(self):
if self.ACTIVE_JWKS is not None:
from canaille.oidc.jose import has_rsa_key
if not has_rsa_key(self.ACTIVE_JWKS):
raise ValueError(
"OIDC specification requires RS256 support. "
"Please configure at least one RSA key in ACTIVE_JWKS."
)
return self