*/
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()) {
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()->get('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 = <<
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