*/ class AuthController extends AbstractActionController { const AUTH_TYPE_LOCAL = 'local'; const AUTH_TYPE_LOCAL_DB = 'db'; const AUTH_TYPE_LOCAL_LDAP = 'ldap'; const AUTH_TYPES_LOCAL = [self::AUTH_TYPE_LOCAL_DB, self::AUTH_TYPE_LOCAL_LDAP]; const AUTH_TYPE_TOKEN = 'token'; const AUTH_TYPE_QUERY_PARAM = 'authtype'; use ShibServiceAwareTrait; use UserServiceAwareTrait; use UserContextServiceAwareTrait; use ModuleOptionsAwareTrait; /** * @var string */ protected $defaultAuthType = self::AUTH_TYPE_LOCAL_DB; /** * @var LoginForm[] ['type' => LoginForm] */ protected $loginFormForType; /** * @var callable $redirectCallback */ protected $redirectCallback; /** * @param callable $redirectCallback * @return self */ public function setRedirectCallback(callable $redirectCallback): self { $this->redirectCallback = $redirectCallback; return $this; } /** * @param string $type * @return LoginForm */ public function getLoginFormForType(string $type): LoginForm { if ($type === self::AUTH_TYPE_LOCAL) { $type = $this->defaultAuthType; } if (! isset($this->loginFormForType[$type])) { throw new RuntimeException("Pas de formulaire spécifié pour le type '$type'"); } return $this->loginFormForType[$type]; } /** * @param LoginForm $loginForm * @return self */ public function addLoginForm(LoginForm $loginForm): self { foreach ($loginForm->getTypes() as $type) { $this->loginFormForType[$type] = $loginForm; } return $this; } /** * @var string */ protected $failedLoginMessage = "L'authentification a échoué, merci de réessayer."; /** * Login form */ public function loginAction() { if ($this->zfcUserAuthentication()->hasIdentity()) { $roleId = $this->params()->fromPost('role', $this->params()->fromQuery('role', false)); if ($roleId) { $this->serviceUserContext->setSelectedIdentityRole($roleId); } if ($this->getRequestedRedirect()) { $redirect = $this->redirectCallback; return $redirect(); } return $this->redirect()->toRoute($this->moduleOptions->getLoginRedirectRoute()); } $typeFromRoute = $this->params('type'); $typeFromRequest = $this->getRequestedAuthenticationType(); $type = $this->processedType($typeFromRequest); if ($type !== $typeFromRoute) { return $this->redirect()->toRoute(null, ['type' => $type], ['query' => $this->params()->fromQuery()], true); } $request = $this->getRequest(); $form = $this->getLoginFormForType($type); $form->initFromRequest($request); // si le formulaire POSTé ne possède aucun champ identifiant, on va directement à authenticateAction() if ($request->isPost() and ! $request->getPost()->offsetExists('identity')) { return $this->redirect()->toRoute('zfcuser/authenticate', [], ['query' => $this->params()->fromQuery()], true); } $redirect = $this->getRequestedRedirect(); $roleId = $this->params()->fromPost('role', $this->params()->fromQuery('role', false)); $queryParams = array_filter([ 'redirect' => $redirect ?: null, 'role' => $roleId ?: null, ]); $url = $this->url()->fromRoute(null, [], ['query' => $queryParams], true); $form->setAttribute('action', $url); if (!$request->isPost()) { return array( 'types' => $this->moduleOptions->getEnabledAuthTypes(), 'type' => $type, 'loginForm' => $form, 'forms' => $this->loginFormForType, 'redirect' => $redirect, 'enableRegistration' => $this->moduleOptions->getEnableRegistration(), ); } $form->setData($request->getPost()); if (!$form->isValid()) { $this->flashMessenger()->setNamespace('zfcuser-login-form')->addMessage($this->failedLoginMessage); return $this->redirect()->toUrl($url); } // clear adapters $this->zfcUserAuthentication()->getAuthAdapter()->resetAdapters(); $this->zfcUserAuthentication()->getAuthService()->clearIdentity(); return $this->authenticateAction(); } /** * @return string|null */ protected function getRequestedAuthenticationType(): ?string { // si un type est spécifié dans la route, on prend if ($requestedType = $this->params('type')) { return $requestedType; } $requestedType = null; // un type d'auth peut être demandé dans l'URL de redirection if ($redirect = $this->getRequestedRedirect()) { parse_str(parse_url(urldecode($redirect), PHP_URL_QUERY), $queryParams); if (isset($queryParams[self::AUTH_TYPE_QUERY_PARAM])) { $requestedType = $queryParams[self::AUTH_TYPE_QUERY_PARAM]; } } return $requestedType; } /** * @return string|null */ protected function getRequestedRedirect(): ?string { if (! $this->moduleOptions->getUseRedirectParameterIfPresent()) { return null; } return $this->params()->fromQuery('redirect'); } /** * @param string|null $type * @return string */ private function processedType(string $type = null): string { if ($type === self::AUTH_TYPE_LOCAL) { return $type; } $enabledTypes = array_keys($this->moduleOptions->getEnabledAuthTypes()); // types d'auth activés // si aucun type n'est spécifié dans la requête ou si le type n'est pas activé, on prend le 1er type activé. if (! in_array($type, $enabledTypes)) { $type = reset($enabledTypes); } // type spécial pour les modes d'authentification nécessitant un formulaire username/password if (in_array($type, self::AUTH_TYPES_LOCAL)) { $type = self::AUTH_TYPE_LOCAL; } return $type; } /** * General-purpose authentication action */ public function authenticateAction() { if ($this->zfcUserAuthentication()->hasIdentity()) { return $this->redirect()->toRoute($this->moduleOptions->getLoginRedirectRoute()); } $type = $this->params('type'); $adapter = $this->zfcUserAuthentication()->getAuthAdapter(); $redirect = $this->params()->fromPost('redirect', $this->params()->fromQuery('redirect', false)); $roleId = $this->params()->fromPost('role', $this->params()->fromQuery('role', false)); $request = $this->getRequest(); $request->getPost()->set('type', $type); $result = $adapter->prepareForAuthentication($request); // Return early if an adapter returned a response if ($result instanceof ResponseInterface) { return $result; } $auth = $this->zfcUserAuthentication()->getAuthService()->authenticate($adapter); if ($roleId) { $this->serviceUserContext->setNextSelectedIdentityRole($roleId); } if (!$auth->isValid()) { $message = $auth->getMessages()[0] ?? $this->failedLoginMessage; $this->flashMessenger()->setNamespace('zfcuser-login-form')->addMessage($message); $adapter->resetAdapters(); $queryParams = array_filter([ 'redirect' => $redirect ?: null, 'role' => $roleId ?: null, ]); $url = $this->url()->fromRoute(null, [], ['query' => $queryParams], true); return $this->redirect()->toUrl($url); } $redirect = $this->redirectCallback; return $redirect(); } /** * Logout and clear the identity */ public function logoutAction(): ResponseInterface { $chain = $this->zfcUserAuthentication()->getAuthAdapter(); $service = $this->zfcUserAuthentication()->getAuthService(); $chain->resetAdapters(); /** * @see LocalAdapter::logout() * @see Cas::logout() * @see Shib::logout() */ $result = $chain->logoutAdapters(); $service->clearIdentity(); if ($result instanceof ResponseInterface) { return $result; } $redirect = $this->redirectCallback; return $redirect(); } /** * Cette action peut être appelée lorsque l'authentification Shibboleth est activée * (unicaen-auth.shibboleth.enable === true). * * > Si la config Apache du module Shibboleth est correcte sur le serveur d'appli, une requête à l'adresse * correspondant à cette action sera détournée par Apache pour réaliser l'authentification Shibboleth. * Une fois l'authentification réalisée avec succès, le Apache renvoie une nouvelle requête * à l'adresse correspondant à cette action, et l'utilisateur authentifié est disponible via * {@see ShibService::getAuthenticatedUser()}. * * > Par contre, si la config Apache du module Shibboleth est incorrecte ou absente (sur votre machine de dev par * exemple), alors : * - si la simulation Shibboleth est activée dans la config du module unicaen/auth * (unicaen-auth.shibboleth.simulate), c'est l'utilisateur configurée qui sera authentifié ; * - sinon, une page d'aide s'affichera indiquant que la config Apache du module Shibboleth est sans doute * erronée. * * @return Response|array */ public function shibbolethAction() { $shibUser = $this->shibService->getAuthenticatedUser(); // NB: si la simulation d'authentification est activée (cf. config), $shibUser !== null. if ($shibUser === null) { return []; // affichage d'une page d'aide } // URL vers laquelle rediriger une fois l'authentification réussie $redirectUrl = $this->params()->fromQuery('redirect', '/'); return $this->redirect()->toUrl($redirectUrl); } /** * @return Response|ViewModel */ public function requestPasswordResetAction() { $form = $this->userService->createResetPasswordEmailForm(); $view = new ViewModel(); $view->setVariable('form', $form); $view->setTemplate('unicaen-auth/auth/request-password-reset-form'); /** @var Request $request */ $request = $this->getRequest(); if ($request->isPost()) { $data = $request->getPost(); $form->setData($data); if ($form->isValid()) { $email = $data['email']; try { $this->processPasswordResetRequest($email); $view->setVariable('email', $email); $view->setTemplate('unicaen-auth/auth/request-password-reset-success'); } catch (DomainException $de) { // affichage de l'erreur comme une erreur de validation $form->get('email')->setMessages([$de->getMessage()]); } } } return $view; } /** * @param string $email */ private function processPasswordResetRequest(string $email) { // Recherche de l'utilisateur ayant pour *username* (login) l'email spécifié $user = $this->userService->getUserMapper()->findOneByUsername($email); if ($user === null) { // Aucun utilisateur trouvé ayant l'email spécifié : // on ne fait rien mais on ne le signale pas sinon le formulaire permettrait // de tester si des emails potentiellement valides existent dans la base. return; } if (! $user->isLocal()) { // L'email spécifié appartient à un utilisateur non local : on signale l'impossibilité de changer le mdp. throw new DomainException("Le changement de mot de passe n'est pas possible pour cet utilisateur."); } // génération/enregistrement d'un token $token = $this->userService->updateUserPasswordResetToken($user); // envoi du mail contenant le lien de changement de mdp $app = $this->appInfos()->getNom(); $subject = "[$app] Demande de changement de mot de passe"; $changePasswordUrl = $this->url()->fromRoute('auth/changePassword', ['token' => $token], ['force_canonical' => true]); $body = <<Une demande de changement de mot de passe a été faite sur l'application $app.

Si vous n'en êtes pas l'auteur, vous pouvez ignorer ce message.

Cliquez sur le lien suivant pour accéder au formulaire de changement de votre mot de passe :
$changePasswordUrl

EOS; $message = $this->mail()->createNewMessage($body, $subject); $message->setTo($email); $this->mail()->send($message); } /** * @return array|ViewModel */ public function changePasswordAction() { $token = $this->params()->fromRoute('token'); $view = new ViewModel(); // recherche du token spécifié dans table utilisateur $user = $this->userService->getUserMapper()->findOneByPasswordResetToken($token); if ($user === null) { // token inexistant $view->setVariable('result', 'unknown_token'); $view->setTemplate('unicaen-auth/auth/change-password-result'); return $view; } $form = $this->userService->createPasswordChangeForm(); /** @var Request $request */ $request = $this->getRequest(); if ($request->isPost()) { $data = $request->getPost(); $form->setData($data); if ($form->isValid()) { // màj password $password = $this->params()->fromPost('password'); $this->userService->updateUserPassword($user, $password); $view->setVariable('result', 'success'); $view->setTemplate('unicaen-auth/auth/change-password-result'); // todo: faut-il déconnecter l'utilisateur (attention au logout shib différent) ? return $view; } } // test durée de vie du token $date = $this->userService->extractDateFromResetPasswordToken($token); if ($date < date_create()) { // token expiré, on le raz $this->userService->clearUserPasswordResetToken($user); $view->setVariable('result', 'dead_token'); $view->setTemplate('unicaen-auth/auth/change-password-result'); return $view; } $view->setVariable('form', $form); $view->setTemplate('unicaen-auth/auth/change-password-form'); return $view; } }