AuthentificationManager.java

/*
 * Copyright (C) 2000 - 2025 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception.  You should have received a copy of the text describing
 * the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.silverpeas.mobile.client.common;

import com.google.gwt.core.client.GWT;
import com.google.gwt.http.client.*;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window;
import com.googlecode.gwt.crypto.bouncycastle.InvalidCipherTextException;
import com.googlecode.gwt.crypto.client.TripleDesCipher;
import org.silverpeas.mobile.client.SpMobil;
import org.silverpeas.mobile.client.common.event.ErrorEvent;
import org.silverpeas.mobile.client.common.event.authentication.AuthenticationErrorEvent;
import org.silverpeas.mobile.client.common.navigation.PageHistory;
import org.silverpeas.mobile.client.pages.connexion.TwoFactorPage;
import org.silverpeas.mobile.client.pages.connexion.TwoFactorEnrollmentPage;
import org.silverpeas.mobile.client.common.network.NetworkHelper;
import org.silverpeas.mobile.client.common.network.rest.RestCallback;
import org.silverpeas.mobile.client.common.network.rest.RestMethod;
import org.silverpeas.mobile.client.common.network.rest.RestMethodCallbackOnlineOnly;
import org.silverpeas.mobile.client.common.resources.ResourcesManager;
import org.silverpeas.mobile.client.common.storage.LocalStorageHelper;
import org.silverpeas.mobile.client.common.storage.CacheStorageHelper;
import org.silverpeas.mobile.shared.dto.DetailUserDTO;
import org.silverpeas.mobile.shared.dto.FullUserDTO;
import org.silverpeas.mobile.shared.dto.IFullUser;
import org.silverpeas.mobile.shared.dto.authentication.IUserProfile;
import org.silverpeas.mobile.shared.dto.authentication.UserProfileDTO;
import org.silverpeas.mobile.shared.exceptions.AuthenticationException;


/**
 * @author: svu
 */
public class AuthentificationManager {

  private static AuthentificationManager instance = null;
  private static final String USER_CONNECTED_KEY = "userConnected";
  public static final String USER_PROFIL = "userProfil";

  public static final String XSTKN = "X-STKN";
  public static final String LOCAL_CREDENTIAL = "localCredential";
  public static final String XSilverpeasSession = "X-Silverpeas-Session";

  public static AuthentificationManager getInstance() {
    if (instance == null) {
      instance = new AuthentificationManager();
    }
    return instance;
  }

  public void addHeader(String key, String value) {
    LocalStorageHelper.getInstance().store(key, value);
  }

  public String getHeader(String key) {
    return LocalStorageHelper.getInstance().load(key);
  }

  public void storeUser(final DetailUserDTO user, final UserProfileDTO profil, String login,
                        String password, String domainId) {
    String encryptedPassword = null;
    try {
      encryptedPassword = encryptPassword(password);
    } catch (InvalidCipherTextException e) {
      EventBus.getInstance().fireEvent(new ErrorEvent(e));
    }
    SpMobil.setUser(user, false);

    FullUserDTO u = new FullUserDTO(login, encryptedPassword, domainId, user);

    String maintainSession = ResourcesManager.getParam("maintain.session");
    if (maintainSession.equalsIgnoreCase("true")) {
      LocalStorageHelper.getInstance().store(USER_CONNECTED_KEY, u.getAutoBean());
      LocalStorageHelper.getInstance().store(USER_PROFIL, profil.getAutoBean());
    }
  }

  public void updateAvatarInCache(final String avatarData) {
    SpMobil.getUser().setAvatar(avatarData);
    SpMobil.getUserProfile().setAvatar(avatarData);
    FullUserDTO user = FullUserDTO.getBean(LocalStorageHelper.getInstance().load(USER_CONNECTED_KEY, IFullUser.class));
    user.setAvatar(avatarData);
    String maintainSession = ResourcesManager.getParam("maintain.session");
    if (maintainSession.equalsIgnoreCase("true")) {
      LocalStorageHelper.getInstance().store(USER_CONNECTED_KEY, user.getAutoBean());
      LocalStorageHelper.getInstance().store(USER_PROFIL, SpMobil.getUserProfile().getAutoBean());
    }
  }

  /**
   * Clean data in local storage while preserving local credentials.
   */
  public void clearLocalStorage() {
    LocalStorageHelper.getInstance().clearExcept(LOCAL_CREDENTIAL);
  }

  public FullUserDTO loadUser() {
    FullUserDTO user = FullUserDTO.getBean(LocalStorageHelper.getInstance().load(USER_CONNECTED_KEY, IFullUser.class));
    SpMobil.setUser(user, false);
    UserProfileDTO profil =
            UserProfileDTO.getBean(LocalStorageHelper.getInstance().load(USER_PROFIL, IUserProfile.class));
    SpMobil.setUserProfile(profil);
    return user;
  }

  /**
   * Decrypt password in local storage.
   *
   * @param passwordEncrysted
   * @return
   */
  public String decryptPassword(String passwordEncrysted) {
    TripleDesCipher cipher = new TripleDesCipher();
    cipher.setKey(ResourcesManager.getParam("mobile.credentials.encryption.key").getBytes());
    String plainPassword = null;
    try {
      plainPassword = cipher.decrypt(passwordEncrysted);
    } catch (Exception e) {
      EventBus.getInstance().fireEvent(new ErrorEvent(e));
    }
    return plainPassword;
  }

  private String encryptPassword(String password) throws InvalidCipherTextException {
    TripleDesCipher cipher = new TripleDesCipher();
    cipher.setKey(ResourcesManager.getParam("mobile.credentials.encryption.key").getBytes());
    String encryptedPassword = cipher.encrypt(password);
    return encryptedPassword;
  }

  public void authenticateOnSilverpeas(String login, String password, String domainId,
                                       Command attempt) {
    Notification.activityStart();
    if (NetworkHelper.isOnline()) {
      ServicesLocator.getRestServiceAuthentication(login, password, domainId).authentication(
          new RestCallback<UserProfileDTO>() {
            @Override
            public void onFailure(final RestMethod method, final Throwable throwable) {
              String enrollmentRequired = method.getHeaders() == null
                  ? null : method.getHeaders().get("X-Silverpeas-2FA-Enrollment-Required");
              if (method.getStatusCode() == 401 &&
                  "true".equalsIgnoreCase(enrollmentRequired)) {
                showEnrollmentPage(login, password, domainId);
                return;
              }
              String twoFactorRequired = method.getHeaders() == null
                  ? null
                  : method.getHeaders().get("X-Silverpeas-2FA-Required");
              if (method.getStatusCode() == 401 &&
                  "true".equalsIgnoreCase(twoFactorRequired)) {
                authenticateTrustedDevice(login, password, domainId, attempt);
                return;
              }
              handleAuthenticationFailure(login, domainId, method, throwable);
            }

            @Override
            public void onSuccess(final RestMethod method, final UserProfileDTO userProfile) {
              completeAuthentication(login, password, domainId, method, userProfile, attempt);
            }
          });
    } else {
      Notification.activityStop();
      loadUser();
      SpMobil.displayMainPage();
    }
  }

  public void authenticateTwoFactor(final String login, final String password,
                                    final String domainId, final String code, final boolean trustDevice,
                                    final Command attempt) {
    authenticateTwoFactor(login, password, domainId, code, trustDevice, attempt, null);
  }

  public void authenticateTwoFactor(final String login, final String password,
      final String domainId, final String code, final boolean trustDevice,
      final Command attempt, final Command onInvalidCode) {
    Notification.activityStart();
    ServicesLocator.getRestServiceAuthentication(login, password, domainId)
            .authenticateTwoFactor(code, trustDevice, new RestCallback<UserProfileDTO>() {
              @Override
              public void onFailure(final RestMethod method, final Throwable throwable) {
                Notification.activityStop();
                if (method.getStatusCode() == 401) {
                  if (onInvalidCode != null) {
                    onInvalidCode.execute();
                    return;
                  }
                  EventBus.getInstance().fireEvent(new AuthenticationErrorEvent(throwable));
                } else {
                  EventBus.getInstance().fireEvent(new ErrorEvent(throwable));
                }
              }

              @Override
              public void onSuccess(final RestMethod method, final UserProfileDTO userProfile) {
                completeAuthentication(login, password, domainId, method, userProfile, attempt);
              }
            });
  }

  private void authenticateTrustedDevice(final String login, final String password,
                                         final String domainId, final Command attempt) {
    ServicesLocator.getRestServiceAuthentication(login, password, domainId)
            .authenticateTrustedDevice(new RestCallback<UserProfileDTO>() {
              @Override
              public void onFailure(final RestMethod method, final Throwable throwable) {
                showTwoFactorPage(login, password, domainId);
              }

              @Override
              public void onSuccess(final RestMethod method, final UserProfileDTO userProfile) {
                completeAuthentication(login, password, domainId, method, userProfile, attempt);
              }
            });
  }

  private void showEnrollmentPage(final String login, final String password,
      final String domainId) {
    Notification.activityStop();
    com.google.gwt.user.client.ui.RootPanel.get().clear();
    com.google.gwt.user.client.ui.RootPanel.get().add(
        new TwoFactorEnrollmentPage(login, password, domainId));
  }

  public void completeEnrollment(final String login, final String password,
      final String domainId, final RestMethod method, final UserProfileDTO profile) {
    completeAuthentication(login, password, domainId, method, profile, null);
  }

  private void showTwoFactorPage(final String login, final String password,
                                 final String domainId) {
    Notification.activityStop();
    TwoFactorPage page = new TwoFactorPage();
    page.setIds(login, password, domainId);
    com.google.gwt.user.client.ui.RootPanel.get().clear();
    com.google.gwt.user.client.ui.RootPanel.get().add(page);
  }

  private void completeAuthentication(final String login, final String password,
                                      final String domainId, final RestMethod authenticationMethod,
                                      final UserProfileDTO userProfile, final Command attempt) {
    addHeader(XSTKN, authenticationMethod.getHeaders().get(XSTKN));
    addHeader(XSilverpeasSession, authenticationMethod.getHeaders().get(XSilverpeasSession));
    SpMobil.setUserProfile(userProfile);

    RestMethodCallbackOnlineOnly action = new RestMethodCallbackOnlineOnly<DetailUserDTO>() {
      @Override
      public void attempt() {
        super.attempt();
        ServicesLocator.getServiceConnection().login(this);
      }

      @Override
      public void onSuccess(final RestMethod method, final DetailUserDTO user) {
        super.onSuccess(method, user);
        SpMobil.setUser(user, true);
        ServicesLocator.getServiceTermsOfService().show(new RestMethodCallbackOnlineOnly<String>() {
          @Override
          public void onFailure(final RestMethod method, final Throwable throwable) {
            if (throwable instanceof AuthenticationException) {
              EventBus.getInstance().fireEvent(new AuthenticationErrorEvent(throwable));
            } else {
              EventBus.getInstance().fireEvent(new ErrorEvent(throwable));
            }
          }

          @Override
          public void onSuccess(final RestMethod method, final String showTermsOfServices) {
            Notification.activityStop();
            AuthentificationManager.getInstance().storeUser(user, userProfile, login,
                    password, domainId);
            if (Boolean.parseBoolean(showTermsOfServices)) {
              SpMobil.displayTermsOfServicePage();
            } else if (attempt == null) {
              SpMobil.displayMainPage();
            } else {
              attempt.execute();
            }
          }
        });
      }

      @Override
      public void onFailure(final RestMethod method, final Throwable t) {
        if (t instanceof AuthenticationException) {
          EventBus.getInstance().fireEvent(new AuthenticationErrorEvent(t));
        } else {
          EventBus.getInstance().fireEvent(new ErrorEvent(t));
        }
      }
    };
    action.attempt();
  }


  private void handleAuthenticationFailure(final String login, final String domainId,
                                           final RestMethod method, final Throwable throwable) {
    int statusError = method.getStatusCode();
    if (statusError == 403) {
      Notification.activityStop();
      SpMobil.displayMainPage();
    } else if (statusError == 401 || statusError == 500) {
      RestMethodCallbackOnlineOnly action = new RestMethodCallbackOnlineOnly<Boolean>() {
        @Override
        public void attempt() {
          super.attempt();
          ServicesLocator.getServiceConnection().userExist(login, domainId, this);
        }

        @Override
        public void onFailure(final RestMethod method, final Throwable t) {
          super.onFailure(method, t);
          EventBus.getInstance().fireEvent(new AuthenticationErrorEvent(t));
        }

        @Override
        public void onSuccess(final RestMethod method, final Boolean exist) {
          super.onSuccess(method, exist);
          AuthenticationException ex = new AuthenticationException(
                  exist ? AuthenticationException.AuthenticationError.PwdNotAvailable :
                          AuthenticationException.AuthenticationError.LoginNotAvailable);
          ex.setLogin(login);
          EventBus.getInstance().fireEvent(new AuthenticationErrorEvent(ex));
        }
      };
      action.attempt();
    } else {
      EventBus.getInstance().fireEvent(new ErrorEvent(throwable));
    }
  }


  public void injectAuthenticationHttpHeaders(RequestBuilder builder) {
    if (getHeader(AuthentificationManager.XSTKN) != null) {
      builder.setHeader(AuthentificationManager.XSTKN, getHeader(AuthentificationManager.XSTKN));
    }
    if (getHeader(AuthentificationManager.XSilverpeasSession) != null) {
      builder.setHeader(AuthentificationManager.XSilverpeasSession,
              getHeader(AuthentificationManager.XSilverpeasSession));
    }
  }

  public void logout() {
    // logout
    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, "/silverpeas/Logout");
    try {
      clearCache();
      builder.sendRequest("", new RequestCallback() {
        @Override
        public void onResponseReceived(final Request request, final Response response) {
          Notification.activityStop();
          AuthentificationManager.getInstance().clearLocalStorage();
          PageHistory.getInstance().clear();
          Notification.activityStop();
          SpMobil.displayLoginPage(null);
          SpMobil.destroyMainPage();
        }
        @Override
        public void onError(final Request request, final Throwable throwable) {
          LocalStorageHelper.getInstance().storeBuildDate();
        }
      });
    } catch (RequestException e) {
      Window.alert(e.getMessage());
    }
  }

  public void clearCache() {
    // Clear offline cache without clearing the session or authentication cookies.
    CacheStorageHelper.clear();
  }
}