Skip to content

Login Assistant

The Login Assistant is a helper class that assists developers in creating a database-driven login system, without the hassle of writing the database access code.

These are the steps to include it in your server side Extension:

  • configure the DBManager in your application's Zone
  • instantiate the component in the init() method of your Extension
  • configure the component

Basic Usage

The first step is to configure the database access in SmartFoxServer 3 by opening the AdminTool > Zone Configurator and selecting the Database Manager tab (check this page for a detailed walkthrough).

For this example we will use a simple users table with the following fields:

id name pword email
1 Kermit thefrog kermit@muppets.com
2 Gonzo thegreat gonzo@muppets.com
3 Fozzie thebear fozzie@muppets.com
4 Rowlf thedog rowlf@muppets.com

Next step we create a new Extension and use this code in the init() method:

public class MuppetsExtension extends SFSExtension
{
    private LoginAssistantComponent lac;

    @Override
    public void init()
    {
        lac = new LoginAssistantComponent(this);

        // Configure the component
        lac.getConfig().loginTable = "muppets";
        lac.getConfig().userNameField = "name";
        lac.getConfig().passwordField = "pword";
    }

    public void destroy()
    {
        lac.destroy();
    }
}

All we have done is simply configuring the component by specifying the name of the table to be used and the names of the fields that represent the user name and password. The component is now configured to use the correct fields and will take care of checking the credentials, dispatching errors and sanitizing user data against SQL injections.

Database passwords

For the sake of this example we're using clear text passwords, but in a real world case you would normally at least hash them with SHA256, SHA512 or similar and maybe also add a "salt". We discuss some of these topics in the Sign Up Assistant tutorial.

A slightly more advanced scenario

Let's suppose we have a slightly more complicated scenario where users login with an email address but their user name in the system must be associated with the name field in the database. Also we'd like to make sure that the user name check is case-sensitive.

By default most databases compare strings in a case-insensitive way, unless a specific collation is used. The Login Assistant can be configured to use a specific field in the DB as the login name, let's see how this is done:

@Override
public void init()
{
    lac = new LoginAssistantComponent(this);

    // Configure the component
    lac.getConfig().loginTable = "muppets";
    lac.getConfig().userNameField = "email";
    lac.getConfig().passwordField = "pword";

    lac.getConfig().nickNameField = "name";
    lac.getConfig().useCaseSensitiveNameChecks = true;
}

All it takes is just a couple of extra lines of code to indicate the nick name field and turning on the case-sensitive check.

Database passwords

We recommend to check your database documentation for string comparison rules as the behavior may be different from vendor to vendor.

Advanced features

Let's try to make things a bit more complicated. Suppose our database contains a couple of extra fields: an avatar image and a flag indicating whether the user has moderator privileges.

id name pword email avatar isMod
1 Kermit thefrog kermit@muppets.com kermit.png Y
2 Gonzo thegreat gonzo@muppets.com gonzo.png N
3 Fozzie thebear fozzie@muppets.com fozzie.png Y
4 Rowlf thedog rowlf@muppets.com rowlf.png N

We will store the image name inside the client's Session object and when the USER_JOIN_ZONE event triggers we'll be able to set the image via User Variables. Also we want to be able to set the client as moderator, if the database flag is set.

The Login Assistant provides an easy way to add custom logic before and after the credentials have been checked, via the preProcessPlugin and postProcessPlugin classes. Let's see how this works:

@Override
public void init()
{
    lac = new LoginAssistantComponent(this);

    // Configure the component
    lac.getConfig().loginTable = "muppets";
    lac.getConfig().userNameField = "email";
    lac.getConfig().passwordField = "pword";
    lac.getConfig().nickNameField = "name";
    lac.getConfig().useCaseSensitiveNameChecks = true;

    lac.getConfig().extraFields = List.of("avatar", "isMod");

    lac.getConfig().postProcessPlugin = new ILoginAssistantPlugin ()
    {
        public void execute(LoginData loginData)
        {
            ISFSObject fields = loginData.extraFields;

            String avatarPic = fields.getString("avatar");
            boolean isMod = fields.getString("isMod").equalsIgnoreCase("Y");

            // Store avatar in session object
            loginData.session.setProperty("avatar", avatarPic);

            // Set client as Moderator
            if (isMod)
                loginData.session.setProperty(SFSConstants.PROP_USER_PERMISSION, DefaultPermissionProfile.MODERATOR);
        }
    };
}

We have added a new configuration parameter called extraFields, containing a list of custom field names we want to select at login time.

In the next line we have passed the postProcessPlugin class by declaring it inline, for convenience. The class is required to implement the ILoginAssistantPlugin interface which declares an execute() method.

In this method we are passed all the relevant login data, including the fields extracted from the database (as an SFSObject) and the client's Session. Here we can apply additional logic to the login process without having to write a custom Login handler.

Custom password checking and encryption

When using a non-encrypted connection the SmartFoxServer client API sends your User password as a hash (tied to your unique session Id) to protect it from traffic sniffing.

However in production environments it is highly recommended to turn on SSL/TLS encryption and send your password "in clear" since it's already protected by TLS.

This means that the server Extension will receive the original password, which can then be hashed and checked against what's stored in the database (typically a hash or hash+salt).

@Override
public void init()
{
    lac = new LoginAssistantComponent(this);

    // Configure the component
    lac.getConfig().loginTable = "muppets";

    ...

    lac.getConfig().customPasswordCheck = true;
}

This in turn allows us to send the password as a field of the custom SFSObject in the LoginRequest and handle it in the PreProcess plugin.

This is how the client should send the password:

var sfso = new SFSObject();
sfso.PutShortString("passwd", tf_password.text); // Read it from some text field

sfs.Send(new LoginRequest(ti_userName.text, "", sfs.Config.Zone, sfso));

It is important to note that we pass the password as a custom field of the optional SFSObject in the LoginRequest. The field name can be any identifier as long as the same string is used both from client and server side.

On the server side we must take care of validating the password in the PreProcess plugin:

class LoginPreProcess implements ILoginAssistantPlugin
{
    @Override
    public void execute(LoginData ld)
    {
        String clientPass = ld.clientIncomingData.getShortString("passwd");

        /*
        * Pre-process the client password here before
        * validating it (e.g. apply SHA256 or SHA512)
        */

        // Let's see if the password from the DB matches that of the user
        if (!ld.password.equals(clientPass))
            throw new PasswordCheckException();

        // Success!
    }
}

Once we obtain the client password from the LoginData parameter we should process it, for instance by hashing it and finally validate it. If there's no match we need to raise a PasswordCheckException to interrupt the flow. This in turn will notify the client of using the regular LOGIN_ERROR event.

If the password matches, the Login Assistant flow will continue with the rest of the logic and the client will be logged in.

Handling errors

The errors related to wrong credentials, banned access, duplicate user names, etc, are all handled by the Login Assistant component using the standard SmartFoxServer rules. This means that all error messages can be customized on the client-side via the SFSErrorCodes class. (see the relative tutorial).

The only place where the developer is responsible for handling exceptions is in the pre-process and post-process plugins. Any runtime exception in those classes will travel up to the top of the SFS3 API and will be captured and sent to the log files.

This also means that Exceptions will interrupt the login flow, therefore it is up to the developer to decide which needs to be caught (in order not to interrupt the flow) and which can be let go.