Deux apps étendent le système d'authentification de base de Mayan EDMS :
| App | Fonctionnalité |
|---|---|
mayan/apps/authentication_otp/ |
Second facteur TOTP (Google Authenticator, Aegis, etc.) |
mayan/apps/authentication_oidc/ |
SSO via OpenID Connect (Keycloak, Auth0, etc.) |
Ces apps s'intègrent dans le mécanisme de backends pluggables décrit dans L'application Authentication.
authentication_otp — Second facteur TOTPAjoute une étape TOTP (Time-based One-Time Password, RFC 6238) après la saisie du mot de passe. L'étape est conditionnelle : elle ne s'affiche que si l'utilisateur a activé l'OTP sur son compte.
Librairie utilisée : pyotp.
| Backend | Description |
|---|---|
AuthenticationBackendModelUsernamePasswordTOTP |
Nom d'utilisateur + mot de passe + TOTP |
AuthenticationBackendModelEmailPasswordTOTP |
Email + mot de passe + TOTP |
Ces backends héritent respectivement de AuthenticationBackendModelUsernamePassword / ...EmailPassword (app authentication) et ajoutent AuthenticationFormTOTP à leur form_list.
# config.yml
AUTHENTICATION_BACKEND: mayan.apps.authentication_otp.authentication_backends.AuthenticationBackendModelUsernamePasswordTOTP
AUTHENTICATION_BACKEND_ARGUMENTS:
maximum_session_length: 2592000
UserOTPDataStocke la configuration TOTP par utilisateur (relation OneToOne) :
class UserOTPData(models.Model):
user = OneToOneField(User, related_name='otp_data')
secret = CharField(max_length=96, blank=True) # base32, vide = OTP désactivé
| Méthode | Description |
|---|---|
is_enabled() |
True si secret non vide |
verify_token(token, secret=None) |
Vérifie un token TOTP via pyotp.TOTP |
enable(secret, token) |
Active l'OTP après vérification du token |
disable() |
Vide le secret et émet event_otp_disabled |
UserOTPDataEnableView
→ génère un secret aléatoire (pyotp.random_base32())
→ signe le secret avec django.core.signing.dumps()
→ redirige vers UserOTPDataVerifyTokenView
UserOTPDataVerifyTokenView (FormUserOTPDataEdit)
→ affiche le QR code (pyotp URI) + le secret en clair
→ l'utilisateur scanne avec son appli authenticator
→ saisit un token pour confirmer
→ valide avec pyotp.TOTP(secret).now()
→ si OK : user.otp_data.enable(secret, token)
Le secret est transmis signé (signed_secret dans l'URL) pour éviter la falsification entre les deux étapes. Si la signature est invalide, BadSignature est attrapée et l'activation est annulée.
MayanLoginView
→ étape 1 : nom d'utilisateur + mot de passe
→ stocke user_id en session (_multi_factor_user_id)
MultiFactorAuthenticationView
→ étape 2 : AuthenticationFormTOTP
condition() → vérifie si otp_data.is_enabled() pour cet utilisateur
clean() → authenticate(factor_name='otp_token', otp_token=..., user=...)
condition() rend l'étape TOTP optionnelle : si l'utilisateur n'a pas activé l'OTP, l'étape est sautée automatiquement.
DjangoAuthenticationBackendWrapperMultiFactor orchestre plusieurs backends Django avec des facteurs nommés :
class DjangoAuthenticationBackendUsernameMultiFactorOTP(
DjangoAuthenticationBackendWrapperMultiFactor
):
factors = (
{'default': True, 'class': ModelBackend, 'name': 'username_password'},
{'class': DjangoAuthenticationBackendOTP, 'name': 'otp_token'},
)
authenticate(factor_name='otp_token', ...) dispatche vers le bon backend selon le facteur.
| Événement | Déclencheur |
|---|---|
event_otp_enabled |
OTP activé sur un compte |
event_otp_disabled |
OTP désactivé sur un compte |
authentication_oidc — SSO OpenID ConnectDélègue l'authentification à un fournisseur d'identité OIDC (Keycloak, Auth0, Dex, etc.) via la librairie mozilla-django-oidc. L'utilisateur n'entre pas son mot de passe dans Mayan — il est redirigé vers le fournisseur.
Cette app fait partie du fork
egyptianet contient des modifications locales (vue de déconnexion SSO, contournement CORS).
AUTHENTICATION_BACKEND: mayan.apps.authentication_oidc.authentication_backends.AuthenticationBackendOIDC
AUTHENTICATION_BACKEND_ARGUMENTS:
# Option 1 : découverte automatique via l'URL well-known
oidc_discovery_url: https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration
oidc_discovery_timeout: [5, 15]
# Option 2 : endpoints manuels (si pas de discovery)
oidc_op_authorization_endpoint: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth
oidc_op_token_endpoint: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token
oidc_op_user_endpoint: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/userinfo
oidc_op_jwks_endpoint: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs
oidc_op_logout_endpoint: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/logout
oidc_rp_client_id: mayan-edms
oidc_rp_client_secret: <votre-secret>
oidc_rp_sign_algo: RS256 # ou HS256
Si oidc_discovery_url est fourni, le backend effectue un GET sur l'URL lors de l'__init__() et en extrait automatiquement :
authorization_endpointjwks_uritoken_endpointuserinfo_endpointend_session_endpoint (optionnel)initialize() — ce qui est configuré au démarragesettings.AUTHENTICATION_BACKENDS = (
'DjangoAuthenticationBackendOIDC',
'ModelBackend', # fallback pour les comptes locaux
)
settings.MIDDLEWARE += ('mozilla_django_oidc.middleware.SessionRefresh',)
settings.REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = (
'mozilla_django_oidc.contrib.drf.OIDCAuthentication',
...existing...
)
# Si logout endpoint configuré :
settings.LOGOUT_REDIRECT_URL = '/oidc/sso-logout/'
DjangoAuthenticationBackendOIDCÉtend OIDCAuthenticationBackend (mozilla-django-oidc) pour :
create_user(claims) : crée l'utilisateur Django depuis les claims OIDC, initialise first_name / last_name depuis given_name / family_name. Pose _event_ignore = True pour éviter un double-événement création.update_user(user, claims) : met à jour first_name / last_name à chaque login.Le nom d'utilisateur est généré depuis l'email via generate_username_from_email() (normalisation Unicode NFKC, tronqué à 150 caractères).
MayanLoginView
→ formulaire AuthenticationFormOIDC
→ bouton "Login via OIDC" → GET /oidc/authenticate/ (mozilla_django_oidc)
→ redirection vers Keycloak
Keycloak
→ authentification de l'utilisateur
→ redirection vers /oidc/callback/
MayanOIDCAuthenticationCallbackView (hérite OIDCAuthenticationCallbackView)
→ échange le code contre un token
→ appelle DjangoAuthenticationBackendOIDC.authenticate()
→ create_user() ou update_user() selon si l'utilisateur existe
→ Django login()
→ redirection vers success_url (oidc_login_next ou LOGIN_REDIRECT_URL)
MayanOIDCSSOLogoutView)Problème : Mayan utilise AJAX pour la navigation. Un redirect 302 serveur vers Keycloak est intercepté par jQuery (XHR), déclenchant un preflight CORS que Keycloak refuse.
Solution : La vue /oidc/sso-logout/ retourne une page HTML 200 contenant window.location.replace(keycloak_url). jQuery injecte le script via .html(), qui exécute une vraie navigation navigateur — sans XHR, sans CORS.
Paramètres envoyés à l'endpoint de déconnexion Keycloak :
id_token_hint : token ID stocké en session (requiert OIDC_STORE_ID_TOKEN=true)post_logout_redirect_uri : URL de retour après déconnexionid_token : client_id en fallback (Keycloak 18+)| Variable d'environnement | Défaut | Description |
|---|---|---|
MAYAN_AUTHENTICATION_OIDC_USER_PROFILE_URL |
None |
URL vers le profil utilisateur sur le IdP |
Les settings OIDC_* de mozilla-django-oidc sont configurés via UpstreamSettingCollection dans AUTHENTICATION_BACKEND_ARGUMENTS.
AUTHENTICATION_BACKEND: mayan.apps.authentication_oidc.authentication_backends.AuthenticationBackendOIDC
AUTHENTICATION_BACKEND_ARGUMENTS:
oidc_discovery_url: https://keycloak.cool.local/realms/mayan/.well-known/openid-configuration
oidc_rp_client_id: mayan-edms
oidc_rp_client_secret: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
oidc_rp_sign_algo: RS256
oidc_store_id_token: true
oidc_logout_redirect_uri: https://mayan.cool.local/
AUTHENTICATION_OIDC_USER_PROFILE_URL: https://keycloak.cool.local/realms/mayan/account/
| Backend | App | Identifiant |
|---|---|---|
AuthenticationBackendModelDjangoDefault |
authentication |
mayan.apps.authentication.authentication_backends.AuthenticationBackendModelDjangoDefault |
AuthenticationBackendModelUsernamePassword |
authentication |
...AuthenticationBackendModelUsernamePassword |
AuthenticationBackendModelEmailPassword |
authentication |
...AuthenticationBackendModelEmailPassword |
AuthenticationBackendModelUsernamePasswordTOTP |
authentication_otp |
mayan.apps.authentication_otp.authentication_backends.AuthenticationBackendModelUsernamePasswordTOTP |
AuthenticationBackendModelEmailPasswordTOTP |
authentication_otp |
...AuthenticationBackendModelEmailPasswordTOTP |
AuthenticationBackendOIDC |
authentication_oidc |
mayan.apps.authentication_oidc.authentication_backends.AuthenticationBackendOIDC |