Monday, July 15, 2013

OAM 11g Custom Authentication Plugins: Interacting with the Identity Store


The OAM 11g release includes a powerful authentication plugin framework, which can be used to extend the out-of-the-box authentication schemes, or to implement something completely custom. In this post, we explore how an authentication plugin can interact with the underlying LDAP Identity Store, via a simple example. This post is part of a larger series on Oracle Access Manager 11g called Oracle Access Manager Academy. An index to the entire series with links to each of the separate posts is available.

The problem we're trying to solve

The example we'll use is a relatively simple one. Let's assume that we want to prevent the same user from logging in multiple times, from different IP addresses, within a short period of time. This might be the very first step in solving the sort of fraudulent login problems that would be better addressed using the Oracle Adaptive Access Manager product. The approach we will take here is to persist the IP address and login time to the user's entry in the LDAP store. On subsequent login, we will compare against those values and if the user is attempting to log in from a different IP address, within a 5-minute period, the login attempt will be blocked.

We are going to implement this flow by using a custom authentication plugin within OAM 11g. By tying this custom plugin together with some of the standard plugins in a new Authentication Module, we can avoid having to rewrite existing functionality, yet can be sure that our custom step cannot be bypassed. At this point, there is some required reading, so be sure you've had a look at what the product documentation has to say about authentication plugins as I won't repeat any of that material. Also have a look at the API Reference, which you'll need to refer back to as you go. It may also be instructive to view this post to understand the required OAM configurations steps to assemble the authentication module, since that information won't be repeated here.

Note: The scenario below was built and tested against the PatchSet 1 release of OAM 11g R2 (version 11.1.2.1). The approaches used, though, should be applicable to other 11g R2 releases, although it has not been specifically tested against them. The API calls used are not, however, available in OAM 11g R1.


LDAP schema changes

The changes made to the LDAP schema are not particularly complex; we have added two attributes, IPAddress and LastLoginTime, as well as a single auxiliary object class, OAMLoginUser. Below is the LDIF file I used to load these schema changes into Oracle Unified Directory. It may require some changes to work against a different LDAP server.
dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( 1.3.6.1.4.1.32473.1.1.590
  NAME ( 'IPAddress' )
  DESC 'Last known IP address'
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  SINGLE-VALUE
  X-ORIGIN 'Oracle Unified Directory Server'
  USAGE userApplications )

dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( 1.3.6.1.4.1.32473.1.1.591
  NAME ( 'LastLoginTime' )
  DESC 'Last Login Time'
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
  SINGLE-VALUE
  X-ORIGIN 'Oracle Unified Directory Server'
  USAGE userApplications )

dn: cn=schema
changetype: modify
add: objectClasses
objectClasses: ( 1.3.6.1.4.1.32473.1.1.11
  NAME ( 'OAMLoginUser' )
  DESC 'Aux class to track OAM logins'
  AUXILIARY
  MAY ( IPAddress $LastLoginTime )
  X-ORIGIN 'Oracle Unified Directory Server' )


The custom authentication plugin

Having made the necessary changes to the LDAP schema (and, of course, ensuring that our users have the new object class and attributes added to their entires), we can use a plugin such as the following.

Note:  As always, this is just a code sample to illustrate an approach. It does not do exhaustive error checking, is not intended for production use and is certainly not supported either by Oracle Corporatin or by the author personally.


package com.oracle.ateam.oam.r2ps1plugins;

import java.text.SimpleDateFormat;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import oracle.security.am.engines.common.identity.api.IdentityStoreContext;
import oracle.security.am.plugin.ExecutionStatus;
import oracle.security.am.plugin.MonitoringData;
import oracle.security.am.plugin.authn.AbstractAuthenticationPlugIn;
import oracle.security.am.plugin.authn.AuthenticationContext;
import oracle.security.am.plugin.authn.AuthenticationException;
import oracle.security.am.engines.common.identity.api.*;
import oracle.security.am.plugin.authn.PluginConstants;

public class IPCheckPlugin  extends AbstractAuthenticationPlugIn  {
    public IPCheckPlugin() {
        super();
        System.out.println("IPCheckPlugin:::Just instantiated an IPCheckPlugin");
    }

    public ExecutionStatus process(AuthenticationContext context)
    throws AuthenticationException {
        ExecutionStatus status = ExecutionStatus.FAILURE; 
        System.out.println("IPCheckPlugin:::Just called IPCheckPlugin.process...");       
        IdentityStoreContext isc = context.getIdentityStoreContext();       
        try {
          String userid = (String) context.getCredential().getParam(PluginConstants.KEY_USERNAME).getValue();
          System.out.println("IPCheckPlugin::: userid is  " + userid );
          IdmUser idu = new IdmUser();        
          idu.setUserName(userid);
          List<String> attribs = new ArrayList<String>();
          attribs.add("IPAddress");
          attribs.add("LastLoginTime");
          Map <String, String> attrVals = isc.getUserAttributes(idu, attribs);

          String iPAddress = attrVals.get("IPAddress");
          String lastLoginTime = attrVals.get("LastLoginTime");
          System.out.println("IPCheckPlugin::: Got IP Address value  " + iPAddress );
          System.out.println("IPCheckPlugin::: Got Last Login Time value  " + lastLoginTime );   
          String clientIP = context.getClientIPAddress();        
          System.out.println("IPCheckPlugin::: Got Client IP Address value  " + clientIP );
          SimpleDateFormat sdf = new SimpleDateFormat ("yyyyMMddHHmmZ");
          Date loginTime = new Date();           
          if (clientIP.equals(iPAddress)) {
                isc.modifyUserAttribute(userid, "LastLoginTime", sdf.format(loginTime));
                status = ExecutionStatus.SUCCESS;  
          } else {
                Date lastLogin  = sdf.parse(lastLoginTime);
                if ( ((loginTime.getTime()/60000) - (lastLogin.getTime()/60000)) < 5) {
                    // Two logins in under five minutes from different IP addresses....
                    System.out.println("IPCheckPlugin::: blocking log in attempt" );
                    status = ExecutionStatus.FAILURE; 
                } else {
                    isc.modifyUserAttribute(userid, "LastLoginTime", sdf.format(loginTime));
                    isc.modifyUserAttribute(userid, "IPAddress", clientIP);
                    status = ExecutionStatus.SUCCESS;

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } 
      return status;   
    }

    @Override
    public String getDescription() {
            // TODO Auto-generated method stub
            return "A first Authentication plugin";
    }

    @Override
    public Map<String, MonitoringData> getMonitoringData() {
            // TODO Auto-generated method stub
            return null;
    }

    @Override
    public boolean getMonitoringStatus() {
            // TODO Auto-generated method stub
            return false;
    }

    @Override
    public String getPluginName() {
            return "IPCheckPlugin";
    }

    @Override
    public int getRevision() {
            // TODO Auto-generated method stub
            return 0;
    }

    @Override
    public void setMonitoringStatus(boolean arg0) {
            // TODO Auto-generated method stub

    }
}


Bear in mind that this plugin will execute AFTER we have already done LDAP authentication of the user. We can thus assume that the user exists in the Identity Store and has provided the correct password.

As is generally the case, the important and exciting bit of the code above is the process method (lines 25-68), with the rest being generic and safe to gloss over. It is, of course, useful to review the import statements as well, so that we can understand exactly which plugin API classes are being used here.

Line 29 is the starting point of our interaction with the Identity Store, as we call the AuthenticationContext.getIdentityStoreContext() method to obtain an IdentityStoreContext instance - our link back to the LDAP directory. Next, we need to connect to the directory and retrieve the existing IPAddress and LastLoginTime attribute values for the user. In line 31, we retrieve the user id from the AuthenticationContext credentials, before constructing and initializing an IdmUser object in lines 33-34. In lines 35-37 we create a List containing the attribute names we wish to retrieve, before calling the IdentityStoreContext.getUserAttributes() method in line 38, to retrieve a Map containing the required attribute values for the user.

Once we have the last-known IP Address for the user, we can compare it to the IP Address for the current request, which we obtain using the AuthenticationContext.getClientIPAddress() call in line 44. We then compare the previous IP address from the LDAP directory to the current one in the user request. If these are the same (lines 48-50) we update the LastLoginTime attribute for the user by calling IdentityStoreContext.modifyUserAttribute() and set the plugin return code to SUCCESS, thus allowing the authentication to proceed. If the IP Addresses do not match (lines 51-63), we then check to see if the LastLoginTime is 5 minutes or less before the current time (line 53). If this is the case, we block the login attempt, by setting the plugin return code to FAILURE. If not, we again call  IdentityStoreContext.modifyUserAttribute() to update both the IPAddress and LastLoginTime attributes for the user, before returning SUCCESS.

As mentioned, this is really just a simple example of using the IdentityStoreContext class to read and write user attributes. Have a look around the API documentation to see what else you can do with this framework.

A final note on Class Loading

Since the OAM Authentication Plugin framework is based on Apache Felix, you need to be careful to ensure that your plugin manifest file references the appropriate API packages in order for them to be found at run-time. The sample manifest file from the product documentation will need to be modified in order for this example to work; specifically, you will need to add oracle.security.am.engines.common.identity.api to the Import-Package section of the manifest file, to ensure that the IdentityStoreConext class can be resolved at runtime.

The modified manifest file follows:
Manifest-Version: 1.0
Bundle-Version: 1
Bundle-Name: IPCheckPlugin
Bundle-Activator: com.oracle.ateam.oam.r2ps1plugins.IPCheckPlugin
Bundle-ManifestVersion: 2
Import-Package: org.osgi.framework;version="1.3.0",oracle.security.am.plugin,oracle.security.am.plugin.authn,oracle.security.am.plugin.impl,oracle.security.am.engines.common.identity.api,oracle.security.am.plugin.api,oracle.security.am.common.utilities.principal,oracle.security.idm,javax.security.auth
Bundle-SymbolicName: IPCheckPlugin
 

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.