Password-Based Authentication Pipelines

By default, once a user submits credentials to Liferay Portal, those credentials are checked against Liferay Portal’s database, though you can also delegate authentication to an LDAP server. To use some other system in your environment instead of or in addition to checking credentials against Liferay Portal’s database, you can write an Authenticator and insert it as a step in Liferay Portal’s authentication pipeline.

Because the Authenticator is checked by the Login Portlet, you can’t use this approach if the user must be redirected to the external system or needs a token to authenticate. In those cases, you should use an Auto Login or an Auth Verifier.

Authenticators let you do these things:

  • Log into Liferay Portal with a username and password maintained in an external system
  • Make secondary user authentication checks
  • Perform additional processing when user authentication fails

Read on to learn how to create an Authenticator.

Anatomy of an Authenticator

Authenticators are implemented for various steps in the authentication pipeline. Here are the steps:

  1. auth.pipeline.pre: Comes before default authentication to the Liferay Portal database. In this step, you can instruct Liferay Portal to skip credential validation against the Liferay Portal database. Implemented by Authenticator.

  2. Default (optional) authentication to the Liferay Portal database.

  3. auth.pipeline.post: Further (secondary, tertiary) authentication checks. Implemented by Authenticator.

  4. auth.failure: Perform additional processing after authentication fails. Implemented by AuthFailure.

To create an Authenticator, create a module and add a component that implements the interface:

@Component(
    immediate = true, property = {"key=auth.pipeline.post"},
    service = Authenticator.class
)
public class MyCustomAuth implements Authenticator {

    public int authenticateByEmailAddress(
            long companyId, String emailAddress, String password,
            Map<String, String[]> headerMap, Map<String, String[]> parameterMap)
        throws AuthException {

return Authenticator.SUCCESS;
}

    public int authenticateByScreenName(
            long companyId, String screenName, String password,
            Map<String, String[]> headerMap, Map<String, String[]> parameterMap)
        throws AuthException {

return Authenticator.SUCCESS;
    }

    public int authenticateByUserId(
            long companyId, long userId, String password,
            Map<String, String[]> headerMap, Map<String, String[]> parameterMap)
        throws AuthException {

return Authenticator.SUCCESS;
    }
}

This example has been stripped down so you can see its structure. First, note the @Component annotation’s contents:

  • immediate = true: sets the component to start immediately
  • key=auth.pipeline.post: sets the Authenticator to run in the auth.pipeline.post phase. To run the auth.pipeline.pre phase, substitute auth.pipeline.pre.
  • service = Authenticator.class: implements the Authenticator service. All Authenticators must do this.

The three methods below the annotation run based on how you’ve configured authentication: by email address (the default), by screen name, or by user ID. All the methods throw an AuthException in case the Authenticator is unable to perform its task–perhaps if the system it’s authenticating against is unavailable or if some dependency can’t be found. The methods in this barebones example return success in all cases. If you deploy its module, it has no effect. Naturally, you’ll want to provide more functionality. Next is an example that shows you how to do that.

Creating an Authenticator

This example is an Authenticator that only allows users whose email addresses end with @liferay.com or @example.com. You can implement this using one module that does everything. If you think other modules might be able to use the functionality that validates the email addresses, you might create two modules: one to implement the Authenticator and one to validate email addresses. This example shows the two module approach.

To create an Authenticator, create a module for your implementation. The most appropriate Blade template for this is the service template. Once you have the module, creating the Activator is straightforward:

  1. Add the @Component annotation to bind your Activator to the appropriate authentication pipeline phase.

  2. Implement the Authenticator interface and provide the functionality you need.

  3. Deploy your module. If you’re using Blade CLI, do this via blade deploy.

For this example, you’ll do this twice: once for the email address validator module and once for the Authenticator itself. The Authenticator project contains the interface for the validator, and the validator project contains the implementation. Here’s what the Authenticator module structure looks like:

auth-pipeline-authenticator-project.png

Figure 1: The Authenticator module contains the validator’s interface and the authenticator.

Since the Authenticator is the most relevant, examine it first:

package com.liferay.docs.emailaddressauthenticator;

import java.util.Map;

import com.liferay.docs.emailaddressauthenticator.validator.EmailAddressValidator;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.security.auth.AuthException;
import com.liferay.portal.kernel.security.auth.Authenticator;
import com.liferay.portal.kernel.service.UserLocalService;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;

@Component(
    immediate = true,
    property = {"key=auth.pipeline.post"},
    service = Authenticator.class
)
public class EmailAddressAuthenticator implements Authenticator {

    @Override
    public int authenticateByEmailAddress(long companyId, String emailAddress,
            String password, Map<String, String[]> headerMap,
            Map<String, String[]> parameterMap) throws AuthException {

        return validateDomain(emailAddress);
    }

    @Override
    public int authenticateByScreenName(long companyId, String screenName,
            String password, Map<String, String[]> headerMap,
            Map<String, String[]> parameterMap) throws AuthException {

        String emailAddress = 
            _userLocalService.fetchUserByScreenName(companyId, screenName).getEmailAddress();

        return validateDomain(emailAddress);
    }

    @Override
    public int authenticateByUserId(long companyId, long userId,
            String password, Map<String, String[]> headerMap,
            Map<String, String[]> parameterMap) throws AuthException {

        String emailAddress = 
            _userLocalService.fetchUserById(userId).getEmailAddress();

        return validateDomain(emailAddress);
    }

    private int validateDomain(String emailAddress) throws AuthException {

        if (_emailValidator == null) {

            String msg = "Email address validator is unavailable, cannot authenticate user";         
            _log.error(msg);

            throw new AuthException(msg);
        }

        if (_emailValidator.isValidEmailAddress(emailAddress)) {       
            return Authenticator.SUCCESS;
        }
        return Authenticator.FAILURE;
    }

    @Reference
    private volatile UserLocalService _userLocalService;

    @Reference(
        policy = ReferencePolicy.DYNAMIC,
        cardinality = ReferenceCardinality.OPTIONAL
    )
    private volatile EmailAddressValidator _emailValidator;

    private static final Log _log = LogFactoryUtil.getLog(EmailAddressAuthenticator.class);
}

This time, rather than stubs, the three authentication methods contain functionality. The authenticateByEmailAddress method directly checks the email address provided by the Login Portlet. The other two methods, authenticateByScreenName and authenticateByUserId call Liferay Portal’s UserLocalService to look up the user’s email address before checking it. This service is injected by the OSGi container because of the @Reference annotation. Note that the validator is also injected in this same manner, though it’s configured not to fail if the implementation can’t be found. This allows this module to start regardless of its dependency on the validator implementation. In this case, this is safe because the error is handled by throwing an AuthException and logging the error.

Why would you want to do it this way? To err gracefully. Because this is an auth.pipeline.post Authenticator, you presumably have other Authenticators checking credentials before this one. If this one isn’t working, you want to inform administrators with an error message rather than catastrophically failing and preventing users from logging in.

The only other Java code in this module is the Interface for the validator:

package com.liferay.docs.emailaddressauthenticator.validator;

import aQute.bnd.annotation.ProviderType;

@ProviderType
public interface EmailAddressValidator {

    public boolean isValidEmailAddress(String emailAddress);
}

This defines a single method for checking the email address.

Next, you’ll address the validator module.

auth-pipeline-validator-project.png

Figure 2: The validator project implements the Validator Interface and depends on the authenticator module.

This module contains only one class. It implements the Validator interface:

package com.liferay.docs.emailaddressvalidator.impl;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.osgi.service.component.annotations.Component;
import com.liferay.docs.emailaddressauthenticator.validator.EmailAddressValidator;

@Component(
    immediate = true,
    property = {
    },
    service = EmailAddressValidator.class
)
public class EmailAddressValidatorImpl implements EmailAddressValidator {

    @Override
    public boolean isValidEmailAddress(String emailAddress) {

        if (_validEmailDomains.contains(
            emailAddress.substring(emailAddress.indexOf('@')))) {

            return true;
        }
        return false;
    }

    private Set<String> _validEmailDomains = 
        new HashSet<String>(Arrays.asList(new String[] {"@liferay.com", "@example.com"}));
}

This code checks to make sure that the email address is from the @liferay.com or @example.com domains. The only other interesting part of this module is the Gradle build script, because it defines a compile-only dependency between the two projects. This is divided into two files: a settings.gradle and a build.gradle.

The settings.gradle file defines the location of the project (the Authenticator) the validator depends on:

include ':emailAddressAuthenticator'
project(':emailAddressAuthenticator').projectDir = new File(settingsDir, '../com.liferay.docs.emailAddressAuthenticator')

Since this project contains the interface, it must be on the classpath at compile time, which is when build.gradle is running:

buildscript {
    dependencies {
        classpath group: "com.liferay", name: "com.liferay.gradle.plugins", version: "3.0.23"
    }

    repositories {
        mavenLocal()

        maven {
            url "https://repository-cdn.liferay.com/nexus/content/groups/public"
        }
    }
}

apply plugin: "com.liferay.plugin"

dependencies {
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.0.0"
    compileOnly group: "org.osgi", name: "org.osgi.compendium", version: "5.0.0"

    compileOnly project(":emailAddressAuthenticator")
}

repositories {
    mavenLocal()

    maven {
        url "https://repository-cdn.liferay.com/nexus/content/groups/public"
    }
}

Note the line in the dependencies section that refers to the Authenticator project defined in settings.gradle.

When these projects are deployed, the Authenticator you defined runs, enforcing logins for the two domains specified in the validator.

If you want to examine these projects further, you can download them in this ZIP file.

Related Topics

Auto Login

Writing a Custom Login Portlet

0 (0 Votes)
Auto Login Previous