Jump to: navigation, search

Scout/HowTo/3.9/Extending the login dialog

< Scout‎ | HowTo‎ | 3.9
Revision as of 04:31, 3 July 2013 by Ssw.bsiag.com (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)


Scout
Wiki Home
Website
DownloadGit
Community
ForumsBlogTwitter
Bugzilla
Bugzilla


This how-to describes how to extend the standard login dialog. It is limited to the SWT client but the same principle applies when extending the login dialog for the Swing client.

Background

It might be necessary to extend the LoginDialog either because you want to brand it (Icons, etc) or because you need to ask the user for additional input that is relevant for authentication

The Security Concept page on the Scout Wiki states under the heading Authentication:

Thereby, the default Scout authenticator InternalNetAuthenticator is installed. This can be easily overwritten by registering an OSGi service with the name java.net.Authenticator and a ranking higher than -2. Alternatively, you can register an extension to the Eclipse extension point org.eclipse.core.net.authenticator that contains your custom java.net.Authenticator.

How is this done in detail? This how-to attempts to give an example.

Requirements

For demonstration purposes, let's assume that we want to add the following customisation to the LoginDialog

  • Custom icon on the window border
  • Custom icon on the dialog itself
  • Add two additional text fields that will be used by the SecurityFilter on the server side.
  • These fields shall be stored across login attempts, even if the username/password are not
  • Pre-fill the username with the current username if username/password are not stored across login attempts.
  • In this case, we want the cursor to be in the password field by default


Steps

Client side

In the SWT client plugin.xml add the following extension:

     <extension point="org.eclipse.core.net.authenticator">;
       <authenticator class="org.eclipse.minicrm.ui.swt.login.ExtendedNetAuthenticator">;
       </authenticator>;
    </extension>;

Create a new package org.eclipse.minicrm.ui.swt.login

Create four new classes in this package. To start with, they are copies of the original classes:

  • ExtendedAuthStatus as a copy of org.eclipse.scout.rt.ui.swt.login.internal.AuthStatus
  • ExtendedLoginDialog as a copy of org.eclipse.scout.rt.ui.swt.login.internal.LoginDialog
  • ExtendedNetAuthenticator as a copy of org.eclipse.scout.rt.ui.swt.login.internal.InternalNetAuthenticator
  • ExtendedSecurePreferencesUtility as a copy of org.eclipse.scout.commons.SecurePreferencesUtility

Depending on the customisation you need to do, not all classes might be needed, but for completeness' sake this example makes use of all of them.

We need to add the two new fields, their default values and their getters/setters to the ExtendedAuthStatus class

     public static final String PARAM1_DEFAULT = "test";
    public static final String PARAM2_DEFAULT = "";
 
    private String m_param1 = PARAM1_DEFAULT;
    private String m_param2 = PARAM2_DEFAULT;
 
    public String getParam1() {
      return m_param1;
    }
 
    public String getParam2() {
      return m_param2;
    }
 
    public void setParam1(String param1) {
      m_param1 = param1;
    }
 
    public void setParam2(String param2) {
      m_param2 = param2;
    }

As we want to store the additional parameter independently of the username/password data, we need to add three new methods to ExtendedSecurePreferencesUtility.

     public static void storeData(String path, String param1, String param2) throws StorageException, IOException {
      ISecurePreferences securePreferences = SecurePreferencesFactory.getDefault();
      ISecurePreferences node = securePreferences.node(path);
      node.put("param1", param1, false);
      node.put("param2", param2, false);
      securePreferences.flush();
    }
 
    public static String[] loadData(String path) throws StorageException {
      ISecurePreferences securePreferences = SecurePreferencesFactory.getDefault();
      ISecurePreferences node = securePreferences.node(path);
      String param1 = node.get("param1", ExtendedAuthStatus.PARAM1_DEFAULT);
      String param2 = node.get("param2", ExtendedAuthStatus.PARAM2_DEFAULT);
      return new String[]{param1, param2};
    }
 
    public static void removeData(String path) throws IOException {
      ISecurePreferences securePreferences = SecurePreferencesFactory.getDefault();
      ISecurePreferences node = securePreferences.node(path);
      node.remove("param1");
      node.remove("param2");
      securePreferences.flush();
    }

We also need to modify the existing removeCredentialsmethod:

     public static void removeCredentials(String path) throws IOException {
      ISecurePreferences securePreferences = SecurePreferencesFactory.getDefault();
      ISecurePreferences node = securePreferences.node(path);
      node.remove("username");
      node.remove("password");
      securePreferences.flush();
    }

Next we need to extend the LoginDialog. First we will add the base path for the icons to the class:

   public class ExtendedLoginDialog extends Dialog {
    // TODO: it should be possible to use a relative path here. Where does it start from?
    final String BASE_PATH = "D:/devsbb/workspaces/scout-tutorial/org.eclipse.minicrm.ui.swt/";
 
    // rest of class
  }

Then we will add the window icon first. This needs to be done in the open method:

     public void open() {
      Shell shell = getParent();
      shell.setText(getText());
 
      try {
        final Image icon16 = new Image(shell.getDisplay(), BASE_PATH + "resources/icons/MyIcon16.png");
        final Image icon32 = new Image(shell.getDisplay(), BASE_PATH + "resources/icons/MyIcon32.png");
        final Image icon48 = new Image(shell.getDisplay(), BASE_PATH + "resources/icons/MyIcon64.png");
        final Image icon64 = new Image(shell.getDisplay(), BASE_PATH + "resources/icons/MyIcon64.png");
        final Image icon128 = new Image(shell.getDisplay(), BASE_PATH + "resources/icons/MyIcon128.png");
        final Image icon256 = new Image(shell.getDisplay(), BASE_PATH + "resources/icons/MyIcon256.png");
        final Image[] icons = new Image[]{icon16, icon32, icon48, icon64, icon128, icon256};
        shell.setImages(icons);
      }
      catch (Exception e) {
        System.out.println("Exception while setting image: " + e.getMessage());
      }
 
      createContents(shell);
 
      // rest of class ...
    }

The reason that the icons are added in various sizes is so that the best resolution icon will be used depending on its use (window frame; alt tab display; task bar; etc)

Next we want to add the branded icon to the dialog. We want to place it in place of the empty URL label. This is done in the createContent method. Replace the following code

      Label urlLabel = new Label(shell, SWT.NONE);
      data = new GridData(GridData.HORIZONTAL_ALIGN_END);
      data.horizontalSpan = 1;
      urlLabel.setLayoutData(data);

with the following

      Canvas canvas = new Canvas(shell, SWT.NONE);
      canvas.addPaintListener(new PaintListener() {
        @Override
        public void paintControl(PaintEvent e) {
          Image image = new Image(shell.getDisplay(), BASE_PATH + "resources/icons/MyIcon48.png");
          e.gc.drawImage(image, 0, 0);
        }
      });
      data = new GridData(GridData.HORIZONTAL_ALIGN_END);
      //data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
      data.horizontalSpan = 1;
      data.heightHint = 48;
      data.widthHint = 48;
      canvas.setLayoutData(data);

Next, we want to make sure that the focus is set to the password field if the username field is not empty. Add the following code before pass.addModifyListener()

       if (!StringUtility.isNullOrEmpty(user.getText())) {
        pass.setFocus();
      }

Now, we want to add the two text fields to the dialog, add the following code before if (ExtendedNetAuthenticator.NET_AUTHENTICATION_CACHE_ENABLED) {:

       Label param1Label = new Label(shell, SWT.NONE);
      param1Label.setText(SwtUtility.getNlsText(Display.getCurrent(), "Param1"));
      data = new GridData(GridData.HORIZONTAL_ALIGN_END);
      data.horizontalSpan = 1;
      param1Label.setLayoutData(data);
 
      final Text param1 = new Text(shell, SWT.BORDER | SWT.PASSWORD);
      data = new GridData(GridData.FILL_HORIZONTAL | GridData.HORIZONTAL_ALIGN_BEGINNING);
      data.horizontalSpan = 2;
      data.widthHint = 120;
      param1.setLayoutData(data);
      if (m_status.getParam1() != null) {
        param1.setText(m_status.getParam1());
      }
      param1.addModifyListener(new ModifyListener() {
        @Override
        public void modifyText(ModifyEvent e) {
          m_status.setParam1(param1.getText());
        }
      });
 
      Label param2Label = new Label(shell, SWT.NONE);
      param2Label.setText(SwtUtility.getNlsText(Display.getCurrent(), "Param2"));
      data = new GridData(GridData.HORIZONTAL_ALIGN_END);
      data.horizontalSpan = 1;
      param2Label.setLayoutData(data);
 
      final Text param2 = new Text(shell, SWT.BORDER);
      data = new GridData(GridData.FILL_HORIZONTAL | GridData.HORIZONTAL_ALIGN_BEGINNING);
      data.horizontalSpan = 2;
      data.widthHint = 120;
      param2.setLayoutData(data);
      if (m_status.getParam2() != null) {
        param2.setText(m_status.getParam2());
      }
      param2.addModifyListener(new ModifyListener() {
        @Override
        public void modifyText(ModifyEvent e) {
          m_status.setParam2(param2.getText());
        }
      });

All that remains now is to add support for all this to ExtendedNetAuthenticator

First, we need to retrieve the stored values at the beginning of getPasswordAuthentication. Place the following code before // check auto-login with user-saved credentials:

      try {
        String[] a = ExtendedSecurePreferencesUtility.loadData(path);
        if (a != null) {
          status.setParam1(a[0]);
          status.setParam2(a[1]);
        }
      }
      catch (Throwable t) {
        LOG.error(getRequestingURL().toExternalForm(), t);
      }

Next we need to store the additional parameters after the dialog was closed. Add the following code before if (status.isSavePassword()) {:

         try {
          ExtendedSecurePreferencesUtility.storeData(path, status.getParam1(), status.getParam2());
        }
        catch (Throwable t) {
          LOG.error(getRequestingURL().toExternalForm(), t);
        }

The new LoginDialog now already works. However, the additional parametes are not used for anything yet (except being saved across login attempts)

Passing the parameters to the client

If they were needed on the client side, the easiest way to store them would be to add the corresponding variables to the ClientSession including setters. We could then simply add the following two lines after the call to storeData

         ClientSession.get().setParam1(status.getParam1());
        ClientSession.get().setParam2(status.getParam2());

Passing the parameters to the server

However, as they are needed on the server side, we must find a way to pass them to the server. Ideally, they would be added to the HttpRequest's header (like the username and password). However, as this is not done in the ExtendedNetAuthenticator class, some other way must be found. The easiest solution is probably to either add them to the password, separated by colons (:), they can then easily be extracted by a CustomSecurityFilter. We need to take into account that colons could also be contained in any of the four fields (username, password, param1, param2), so we need to escape them, otherwise they would cause splitting in the wrong places on the server side.

Note: If these parameters are added to the password, a custom implementation of a SecurityFilter is required on the server (see below)

In order to tack our parameters onto the username/password pair, we need to replace all calls of

         return new PasswordAuthentication(status.getUsername(), status.getPassword().toCharArray());

with the following code

         return new PasswordAuthentication(escapeColon(status.getUsername()), addAdditionalParameters(status.getPassword(), status.getParam1(), status.getParam2()).toCharArray());

Of course, we also need to provide these functions:

     private String addAdditionalParameters(String password, String param1, String param2) {
      return escapeColon(password) + ":" + escapeColon(param1) + ":" + escapeColon(param2);
    }
 
    private String escapeColon(String in) {
      // the colon (":") is the field separator to split the fields in the HttpReqeust.header
      // to allow this character to be used in user names, passwords and additional parameters, we need to escape it
      // ASCII 31 was chosen as escape character as it cannot be entered manually but is unlikely to break transfer over HTTP
      String out = in.replace(":", Character.toString((char) 31));
      return out;
    }

Server side

Finally, we need to create our own SecurityProvider on the server side.

Create a new class CustomSecurityFilter, based on the SecurityFilter that most closely covers the needs for your authentication mechanism.

Register the class in the server's plugin.xml file, in the org.eclipse.scout.http.servletfilter.filters extension point:

      <filter
            aliases="/process"
            class="org.eclipse.minicrm.server.services.custom.security.CustomSecurityFilter"
            ranking="42">
      </filter>

You will also need to add the necessary configuration fields into your product's config.ini file (this example assumes you modelled your CustomSecurityFilter on a DataSourceSecurityFilter):

org.eclipse.minicrm.server.services.custom.security.CustomSecurityFilter#active=true
org.eclipse.minicrm.server.services.custom.security.CustomSecurityFilter#jdbcDriverName=org.apache.derby.jdbc.EmbeddedDriver
org.eclipse.minicrm.server.services.custom.security.CustomSecurityFilter#jdbcMappingName=jdbc:derby:D:\\databases\\minicrmDB
org.eclipse.minicrm.server.services.custom.security.CustomSecurityFilter#jdbcUsername=minicrm
org.eclipse.minicrm.server.services.custom.security.CustomSecurityFilter#jdbcPassword=minicrm
org.eclipse.minicrm.server.services.custom.security.CustomSecurityFilter#selectUserPass=SELECT username FROM PERSON WHERE username=? AND password_encoded=?

Now, modify the CustomSecurityFilter's negotiate method to handle the additional parameters. Change the following lines

        String[] a = new String(Base64Utility.decode(h.substring(6)), "ISO-8859-1").split(":", 2);
       String user = a[0].toLowerCase();
       String pass = a[1];

to

        String[] a = new String(Base64Utility.decode(h.substring(6)), "ISO-8859-1").split(":", 4);
       String user = unescapeColon(a[0].toLowerCase());
       String pass = unescapeColon(a[1]);
       String param1 = "";
       String param2 = "";
       if (a.length > 2) {
         param1 = unescapeColon(a[2]);
         if (a.length > 3) {
           param2 = unescapeColon(a[3]);
         }
       }

You will also need to provide this method:

    private String unescapeColon(String in) {
     String out = in.replace(Character.toString((char) 31), ":");
     return out;
   }

You now have the option to use these parameters as part of the authentication mechanism


Result

The extended LoginDialog should look something like this:

LoginDialogExtended.png

Alt tabbing shows it with the properly scaled icon:

LoginDialogExtendedAltTab.png