blob: 2d4ea972f79d4d3c159eb1514aa4e77819a18f2e [file] [log] [blame]
/**
*
* Copyright (c) 2011, 2016 - Loetz GmbH&Co.KG (69115 Heidelberg, Germany)
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Christophe Loetz (Loetz GmbH&Co.KG) - initial implementation
*/
package org.eclipse.osbp.authentication.ui.login;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.mail.Email;
import org.apache.commons.mail.EmailException;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.crypto.CryptoException;
import org.apache.shiro.crypto.PaddingScheme;
import org.apache.shiro.util.ByteSource;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.services.events.IEventBroker;
import org.eclipse.osbp.authentication.account.dtos.UserAccountDto;
import org.eclipse.osbp.authentication.ui.login.BrowserCookie.Callback;
import org.eclipse.osbp.dsl.dto.lib.impl.DtoServiceAccess;
import org.eclipse.osbp.dsl.dto.lib.services.IDTOServiceWithMutablePersistence;
import org.eclipse.osbp.preferences.ProductConfiguration;
import org.eclipse.osbp.ui.api.metadata.IDSLMetadataService;
import org.eclipse.osbp.ui.api.user.IUser;
import org.eclipse.osbp.ui.initialization.IInitializationNotification;
import org.eclipse.osbp.user.User;
import org.eclipse.osbp.utils.theme.EnumCssClass;
import org.eclipse.osbp.vaaclipse.publicapi.authentication.AuthenticationConstants;
import org.eclipse.osbp.vaaclipse.publicapi.resources.BundleResource;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vaadin.event.Action;
import com.vaadin.event.Action.Handler;
import com.vaadin.event.ShortcutAction;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Image;
import com.vaadin.ui.Label;
import com.vaadin.ui.NativeButton;
import com.vaadin.ui.Notification;
import com.vaadin.ui.Notification.Type;
import com.vaadin.ui.Panel;
import com.vaadin.ui.PasswordField;
import com.vaadin.ui.ProgressBar;
import com.vaadin.ui.TextField;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.Window;
/* to be called as login procedure this class must be registered in the product's plugin.xml as extension
* like this:
* <property
name="applicationAuthenticationProvider"
value="bundleclass://org.eclipse.osbp.utils.ui/org.eclipse.osbp.utils.vaaclipse.AuthenticationProvider">
</property>
*/
/* the remember me function stores a cookie named NAME_COOKIE with 40 days validity.
* the contents username and password is crypted using aes encryption.
* the key for encryption is a static with a one time randomized static key
* the key inside this cookie is encrypted using aes with a one time randomized static key
*/
@SuppressWarnings("serial")
public class AuthenticationProvider implements Callback, Handler {
@Inject
private IEventBroker eventBroker;
@Inject
private IEclipseContext context;
@Inject
private IDSLMetadataService dslMetadataService;
/** The Constant LOGGER. */
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationProvider.class);
private AesCipherService cipher;
// this cookie remembers the user credentials
private static final String NAME_COOKIE = "osbp_rememberme"; //$NON-NLS-1$
// this cookie remembers the cipher key
private static final String KEY_COOKIE = "osbp_basic"; //$NON-NLS-1$
// the key to cipher the key
private static final byte[] CIPHER_KEY = Hex.decode("CA69A6B7B1C453A1885DFA8EA5743121");
private static final int KEY_SIZE = 128;
private Locale locale;
private boolean keyWasFound = false;
private Key key;
private Label title;
private TextField userField;
private PasswordField passwordField;
private Button login;
private CheckBox remembermeField;
private Button forgotpasswordField;
private Button register;
private Image userImage;
private Label copyrightField;
private String cookieValue;
private ProgressBar progressValue;
// Have the unmodified Enter key cause an event
Action do_login = new ShortcutAction("Default key", ShortcutAction.KeyCode.ENTER, null);
// Have the C key modified with Alt cause an event
Action action_cancel = new ShortcutAction("Alt+C", ShortcutAction.KeyCode.C, new int[] { ShortcutAction.ModifierKey.ALT });
public AuthenticationProvider() {
InitializationListener.addAuthenticationProvider(this);
}
@PreDestroy
public void preDestroy() {
InitializationListener.removeAuthenticationProvider(this);
}
@PostConstruct
public void init(VerticalLayout parent) {
LOGGER.debug("AuthenticationProvider init");
if (InitializationListener.getUserAccessService() == null) {
LOGGER.error("userAccessService not available");
return;
}
cipher = new AesCipherService();
locale = UI.getCurrent().getLocale();
cipher.setKeySize(KEY_SIZE);
// set polling is necessary due to possible progress bars
UI.getCurrent().setPollInterval(1000);
Panel loginPanel = new Panel();
loginPanel.setWidth("430px");
loginPanel.setId("loginPanelArea");
loginPanel.addStyleName("loginPanelArea os-login");
parent.addComponent(loginPanel);
parent.setComponentAlignment(loginPanel, Alignment.MIDDLE_CENTER);
parent.setPrimaryStyleName("osbp");
loginPanel.setDescription(dslMetadataService.translate(locale.toLanguageTag(), "login_caption_tip"));
VerticalLayout loginForm = new VerticalLayout();
loginForm.setSizeFull();
loginForm.setMargin(true);
loginForm.setId("osbpLoginForm");
loginForm.addStyleName("osbpLoginForm");
loginPanel.setContent(loginForm);
VerticalLayout fullArea = new VerticalLayout();
fullArea.setSizeFull();
fullArea.setId("loginFullArea");
fullArea.addStyleName("loginFullArea");
HorizontalLayout titleArea = new HorizontalLayout();
titleArea.setSizeFull();
titleArea.setId("loginTitelArea");
titleArea.addStyleName("loginTitleArea");
fullArea.addComponent(titleArea);
title = new Label(dslMetadataService.translate(locale.toLanguageTag(), "login_caption"));
title.addStyleName(EnumCssClass.VIEW_HEADER_H2.toString());
titleArea.addComponent(title);
loginForm.addComponent(fullArea);
HorizontalLayout userArea = new HorizontalLayout();
userArea.setId("loginUserArea");
userArea.addStyleName("loginUserArea");
userArea.setSizeFull();
userArea.setMargin(true);
fullArea.addComponent(userArea);
VerticalLayout textArea = new VerticalLayout();
textArea.setSizeFull();
userArea.addComponent(textArea);
userArea.setExpandRatio(textArea, 0.85f);
userArea.setId("loginTextArea");
userArea.addStyleName("loginTextArea");
VerticalLayout imageArea = new VerticalLayout();
imageArea.setSizeFull();
imageArea.setMargin(true);
userArea.addComponent(imageArea);
userArea.setExpandRatio(imageArea, 0.15f);
VerticalLayout buttonArea = new VerticalLayout();
buttonArea.setId("loginButtonArea");
buttonArea.addStyleName("loginButtonArea");
buttonArea.setSizeFull();
buttonArea.setMargin(true);
fullArea.addComponent(buttonArea);
HorizontalLayout loginArea = new HorizontalLayout();
loginArea.setId("loginLoginArea");
loginArea.addStyleName("loginLoginArea");
loginArea.setSizeFull();
buttonArea.addComponent(loginArea);
HorizontalLayout registerArea = new HorizontalLayout();
registerArea.setId("loginRegisterArea");
registerArea.addStyleName("loginRegisterArea");
registerArea.setSizeFull();
buttonArea.addComponent(registerArea);
VerticalLayout copyrightArea = new VerticalLayout();
copyrightArea.setId("loginCopyrightArea");
copyrightArea.addStyleName("loginCopyrightArea");
copyrightArea.setSizeFull();
loginForm.addComponent(copyrightArea);
userImage = new Image(null, BundleResource.valueOf("platform:/plugin/"
+ FrameworkUtil.getBundle(AuthenticationProvider.class).getSymbolicName() + "/assets/padlock.png"));
imageArea.addComponent(userImage);
imageArea.setComponentAlignment(userImage, Alignment.MIDDLE_CENTER);
userField = new TextField();
userField.setSizeFull();
textArea.addComponent(userField);
userField.setInputPrompt(dslMetadataService.translate(locale.toLanguageTag(), "username"));
userField.setDescription(dslMetadataService.translate(locale.toLanguageTag(), "username_tip"));
passwordField = new PasswordField();
passwordField.setSizeFull();
textArea.addComponent(passwordField);
passwordField.setInputPrompt(dslMetadataService.translate(locale.toLanguageTag(), "password"));
passwordField.setDescription(dslMetadataService.translate(locale.toLanguageTag(), "password_tip"));
progressValue = new ProgressBar();
progressValue.setSizeFull();
progressValue.addStyleName("initialization-progress");
progressValue.setVisible(false);
textArea.addComponent(progressValue);
if (!ProductConfiguration.hasNoRememberMe()) {
remembermeField = new CheckBox();
remembermeField.setSizeFull();
loginArea.addComponent(remembermeField);
remembermeField.setCaption(dslMetadataService.translate(locale.toLanguageTag(), "remember_me"));
remembermeField.setDescription(dslMetadataService.translate(locale.toLanguageTag(), "remember_me_tip"));
}
login = new Button();
login.setSizeFull();
loginArea.addComponent(login);
login.setCaption(dslMetadataService.translate(locale.toLanguageTag(), "sign_in"));
login.setDescription(dslMetadataService.translate(locale.toLanguageTag(), "sign_in_tip"));
login.addClickListener(new ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
doLogin();
}
});
forgotpasswordField = new NativeButton();
forgotpasswordField.setSizeFull();
registerArea.addComponent(forgotpasswordField);
forgotpasswordField.setCaption(dslMetadataService.translate(locale.toLanguageTag(), "forgot_password"));
forgotpasswordField.setDescription(dslMetadataService.translate(locale.toLanguageTag(), "forgot_password_tip"));
forgotpasswordField.addClickListener(new ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
doResetPassword();
}
});
register = new Button();
register.setSizeFull();
registerArea.addComponent(register);
register.setCaption(dslMetadataService.translate(locale.toLanguageTag(), "register"));
register.setDescription(dslMetadataService.translate(locale.toLanguageTag(), "register_tip"));
register.addClickListener(new ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
doRegister();
}
});
copyrightField = new Label();
copyrightField.setSizeFull();
copyrightArea.addComponent(copyrightField);
copyrightArea.setComponentAlignment(copyrightField, Alignment.MIDDLE_CENTER);
copyrightField.setValue(dslMetadataService.translate(locale.toLanguageTag(), "copyright"));
// Set this object as the action handler
loginPanel.addActionHandler(this);
LOGGER.debug("AuthenticationProvider layouted");
// eventually encrypt uncrypted passwords
checkCookie();
InitializationListener.addAuthenticationProvider(this);
}
private void checkCookie() {
// examine the cookies
keyWasFound = false;
// keyWasFound will be set by callback function onValueDetected
// when the key cookie was found - onValueDetected starts a new
// detectCookie for the remember-me function
cookieValue = null;
// detectCookie will set this value for a hashCode check
if (!ProductConfiguration.hasNoRememberMe()) {
BrowserCookie.detectCookieValue(KEY_COOKIE, this);
}
LOGGER.debug("AuthenticationProvider cookie detecting finished");
}
private void sendResetPasswordEmail(UserAccountDto user, String resetPassword) {
Email email = AuthenticationUiUtil.getEmail();
email.setSubject(dslMetadataService.translate(locale.toLanguageTag(), "email_password_reset_subject"));
try {
email.setFrom(ProductConfiguration.getAdminEmail());
email.setMsg(dslMetadataService.translate(locale.toLanguageTag(), "email_password_reset_body") + " '" + resetPassword + "'.");
// email.addTo("JoDominguez@compex-commerce.com");
email.addTo(user.getEmail());
email.send();
} catch (EmailException e) {
LOGGER.error("EmailException: " + e.getLocalizedMessage());
}
}
/**
* Retrieve actions for a specific component. This method will be called for
* each object that has a handler; in this example just for login panel. The
* returned action list might as well be static list.
*/
public Action[] getActions(Object target, Object sender) {
return new Action[] { do_login };
}
/**
* Handle actions received from keyboard. This simply directs the actions to
* the same listener methods that are called with ButtonClick events.
*/
public void handleAction(Action action, Object sender, Object target) {
if (action == do_login) {
doLogin();
}
}
public void doLogin() {
checkCookie();
InitializationListener.getUserAccessService().logout();
login.focus();
String username = userField.getValue();
String password = passwordField.getValue();
if (username.trim().isEmpty())
username = null;
if (check(username, password, ProductConfiguration.hasNoRememberMe() ? false : remembermeField.getValue())) {
if (!checkPasswordReset(username)) {
User user = new User(username);
if (context.get(IUser.class) == null) {
context.set(IUser.class, user);
context.set("user", user);
}
eventBroker.send(AuthenticationConstants.Events.Authentication.name, user);
}
}
}
private boolean check(String username, String password, Boolean rememberMe) {
boolean cookieValid = false;
if (cookieValue != null) {
if (!InitializationListener.getUserAccessService().isCookieValid(username, cookieValue)) {
// fraud detected - lock user account
LOGGER.debug("cookie fraud detection - account is locked");
InitializationListener.getUserAccessService().lockAccount(username, true);
}
cookieValid = true;
}
if (!tryToAuthenticate(AuthenticationUiUtil.PORTAL_ID, username, password)) {
if (InitializationListener.getUserAccessService().isAccountLocked(username)) {
Notification.show(dslMetadataService.translate(locale.toLanguageTag(), "lockedmessage"), Type.HUMANIZED_MESSAGE);
} else if (InitializationListener.getUserAccessService().isAccountNotRegistered(username)) {
Notification.show(dslMetadataService.translate(locale.toLanguageTag(), "not_registered_message"), Type.HUMANIZED_MESSAGE);
} else if (!InitializationListener.getUserAccessService().isAccountEnabled(username)) {
Notification.show(dslMetadataService.translate(locale.toLanguageTag(), "disabledmessage"), Type.HUMANIZED_MESSAGE);
} else {
Notification.show(dslMetadataService.translate(locale.toLanguageTag(), "failedmessage"), Type.HUMANIZED_MESSAGE);
}
return false;
}
if (!cookieValid) {
// if login is valid and remember_me is active, store a cookie
if (rememberMe) {
// create a new cipher key if not already found
if (!keyWasFound) {
key = cipher.generateNewKey(KEY_SIZE);
byte[] keyValue = SerializationUtils.serialize(key);
ByteSource encryptedData = null;
try {
cipher.setPaddingScheme(PaddingScheme.PKCS5);
encryptedData = cipher.encrypt(pkcs5Bytes(keyValue), generateKey().getEncoded());
BrowserCookie.setCookie(KEY_COOKIE, Hex.encodeToString(encryptedData.getBytes()), 40 * 24L * 3600L, "/");
keyWasFound = true;
} catch (Exception e) {
Throwable cause = e.getCause();
String msg = e.getLocalizedMessage();
if (cause != null) {
msg += "->" + cause.getLocalizedMessage();
}
LOGGER.error(msg);
}
}
List<String> loginCredentials = new ArrayList<String>();
loginCredentials.add(username);
loginCredentials.add(password);
loginCredentials.add(rememberMe.toString());
String loginData = encryptData(loginCredentials);
if (loginData != null) {
// cookie stays valid up to 40 days
BrowserCookie.setCookie(NAME_COOKIE, loginData, 40 * 24L * 3600L, "/");
InitializationListener.getUserAccessService().setCookieHash(username, loginData);
LOGGER.debug("cookie set for remember me");
}
} else {
// destroy cookie
BrowserCookie.setCookie(NAME_COOKIE, "", 0L, "/");
InitializationListener.getUserAccessService().setCookieHash(username, null);
LOGGER.debug("cookie destroyed for remember me");
}
}
return true;
}
private String encryptData(List<String> loginCredentials) {
cipher.setPaddingScheme(PaddingScheme.PKCS5);
String data = String.join(",", loginCredentials);
ByteSource encryptedData = null;
try {
encryptedData = cipher.encrypt(pkcs5Bytes(data), key.getEncoded());
} catch (CryptoException | UnsupportedEncodingException e) {
Throwable cause = e.getCause();
String msg = e.getLocalizedMessage();
if (cause != null) {
msg += "->" + cause.getLocalizedMessage();
}
LOGGER.error(msg);
return null;
}
return encryptedData.toHex();
}
private List<String> decryptData(String loginData) {
cipher.setPaddingScheme(PaddingScheme.PKCS5);
List<String> credentials = new ArrayList<String>();
ByteSource decryptedData = null;
if (loginData != null) {
try {
decryptedData = cipher.decrypt(Hex.decode(loginData), key.getEncoded());
} catch (CryptoException e) {
Throwable cause = e.getCause();
String msg = e.getLocalizedMessage();
if (cause != null) {
msg += "->" + cause.getLocalizedMessage();
}
LOGGER.error(msg);
return credentials;
}
String decryptedString = new String(decryptedData.getBytes());
String[] data = decryptedString.split(",");
for (String d : data) {
credentials.add(d.trim());
}
}
return credentials;
}
// this callback is called by javascript on detection of the desired cookie
@Override
public void onValueDetected(String cookieName, String value) {
if (cookieName != null && value != null) {
LOGGER.debug("cookie " + cookieName + " was found");
if (KEY_COOKIE.equals(cookieName)) {
try {
// deserialize the decoded hex encoded string
cipher.setPaddingScheme(PaddingScheme.PKCS5);
key = (Key) SerializationUtils.deserialize(cipher.decrypt(Hex.decode(value), generateKey().getEncoded()).getBytes());
keyWasFound = true;
BrowserCookie.detectCookieValue(NAME_COOKIE, this);
} catch (Exception e) {
Throwable cause = e.getCause();
String msg = e.getLocalizedMessage();
if (cause != null) {
msg += "->" + cause.getLocalizedMessage();
}
LOGGER.error(msg);
}
} else if (NAME_COOKIE.equals(cookieName)) {
// store for a later cookie validation
cookieValue = value;
List<String> loginCredentials = decryptData(value);
if (loginCredentials.size() == 3) {
UI.getCurrent().getSession().lock();
userField.setValue(loginCredentials.get(0));
passwordField.setValue(loginCredentials.get(1));
remembermeField.setValue(true);
UI.getCurrent().getSession().unlock();
LOGGER.debug("cookie decoded");
if (ProductConfiguration.hasAutoLogin()) {
login.click();
}
} else {
LOGGER.debug("cookie for remember me has invalid item count");
}
}
} else {
LOGGER.debug("no more valid cookies found");
}
}
/*
* helper function for the PKCS5 algorithm to create a correct padding
* pattern to 16 byte boundaries
*
* the space is filled with the hexadecimal value of the number of padded
* characters
*
* as shiro creates bytecode encoding UTF-8 by default, we must use the same
* for padding characters and the returning byte array
*/
private byte[] pkcs5Bytes(String data) throws UnsupportedEncodingException {
return pkcs5Bytes(data.getBytes("UTF-8"));
}
private byte[] pkcs5Bytes(byte[] data) throws UnsupportedEncodingException {
byte diff = (byte) (16 - data.length % 16);
byte[] result = new byte[data.length + diff];
for (int i = 0; i < data.length + diff; i++) {
if (i < data.length)
result[i] = data[i];
else
result[i] = diff;
}
return result;
}
private static Key generateKey() throws Exception {
Key key = new SecretKeySpec(CIPHER_KEY, "AES");
return key;
}
/**
* Try to authenticate with the credentials given!<br>
* {@link #setAuthenticated(boolean)} will explicit be called!
*
* @param portalId
* @param userName
* @param password
* @return true if the user was authenticated successful
*/
boolean tryToAuthenticate(String portalId, String userName, String password) {
boolean authenticated = InitializationListener.getUserAccessService().isAuthenticated();
if (!authenticated) {
try {
authenticated = InitializationListener.getUserAccessService().authenticate(portalId, userName, password);
} catch (Exception e) {
LOGGER.error(e.getLocalizedMessage());
}
}
return authenticated;
}
public void notifyInitializationStep(IInitializationNotification notification) {
notifiyInitializationStep(notification, true);
}
public void notifyInitializationDone(IInitializationNotification notification) {
notifiyInitializationStep(notification, false);
}
private void notifiyInitializationStep(IInitializationNotification notification, boolean blockUi) {
if (progressValue != null) {
progressValue.setValue((float) notification.getRecommendedProgressValue());
progressValue.setCaption(notification.getRecommendedProgressTitle(true));
progressValue.setVisible(blockUi);
}
enableLoginUI(!blockUi);
}
public void enableLoginUI(boolean reEnableLoginUi) {
if (userField != null) {
userField.setEnabled(reEnableLoginUi);
passwordField.setEnabled(reEnableLoginUi);
login.setEnabled(reEnableLoginUi);
remembermeField.setEnabled(reEnableLoginUi);
register.setEnabled(reEnableLoginUi);
}
}
public void doRegister() {
register.focus();
String username = userField.getValue();
if (username.trim().isEmpty()) {
Notification.show(dslMetadataService.translate(locale.toLanguageTag(), "register_username_is_empty_message"));
userField.focus();
} else if (InitializationListener.getUserAccessService().checkNotLoggedInUsernameExists(username)) {
Notification.show(dslMetadataService.translate(locale.toLanguageTag(), "register_username_already_exists"));
} else { // Do a registration only if the user doesn´t already exist.
User user = new User(username);
if (context.get(User.class) == null) {
context.set(User.class, user);
context.set("user", user);
}
RegistrationDialog registerDialog = new RegistrationDialog();
Window registerWindow = registerDialog.init(eventBroker, dslMetadataService, user);
UI.getCurrent().addWindow(registerWindow);
}
}
public void doResetPassword() {
forgotpasswordField.focus();
String username = userField.getValue();
if (username.trim().isEmpty()) {
username = null;
}
if (InitializationListener.getUserAccessService().checkNotLoggedInUsernameExists(username)) {
UserAccountDto user = (UserAccountDto) InitializationListener.getUserAccessService().findUserAccount(username);
String resetPassword = RandomStringUtils.randomAlphanumeric(8);
@SuppressWarnings("restriction")
IDTOServiceWithMutablePersistence<UserAccountDto> service = (IDTOServiceWithMutablePersistence<UserAccountDto>) DtoServiceAccess
.getService(UserAccountDto.class);
String encryptedPassword = InitializationListener.getUserAccessService().encryptPassword(resetPassword);
user.setPassword(encryptedPassword);
user.setPasswordReset(true);
user.setLocked(false);
user.setFailedAttempt(0);
service.update(user);
Notification.show(dslMetadataService.translate(locale.toLanguageTag(), "password_reset_success_message"));
sendResetPasswordEmail(user, resetPassword);
}
}
public boolean checkPasswordReset(String username) {
// Do a registration only if the user doesn´t already exist.
UserAccountDto userAccount = (UserAccountDto) InitializationListener.getUserAccessService().findUserAccount(username);
if (userAccount != null && userAccount.getPasswordReset()) {
NewPasswordDialog newPasswordDialog = new NewPasswordDialog();
Window registerWindow = newPasswordDialog.init(eventBroker, dslMetadataService, userAccount);
UI.getCurrent().addWindow(registerWindow);
return true;
} else {
return false;
}
}
}