Custom Authentication Module

[ Introduction ] [ This Guide ] [ Architecture ] [ Prerequisites ] [ Step-by-Step Guide to Creating a Custom Authentication Module ] [ Step 1 - create module properties file ] [ Step 2 - create custom login Principal class ] [ Step 3 - create custom login module class ] [ Step 4 - install and configure in Access Manager ] [ Documentation ] [ External Links ] [ Ancillary Files ] [ Contributors ]

Introduction

From time to time, operations engineers, developers, or business owners may need to extend the authentication functionality of [Sun Java System Access Manager] (hereinafter "Access Manager"). Access Manager provides APIs and SPIs for developers to use when extending the Authentication Service. This article provides step-by-step instructions for designing, developing, deploying and testing a Custom Authentication Module.

This article was originally written by Terry Gardner for the Sun Java System wiki.

This Guide

This guide uses the following software:

This article will guide the reader through developing a Custom Authentication Module for Access Manager. The Custom Authentication Module will contact a system on the internet, transmit a user id and passphrase, and read the response. If the response consists of "OK", then the user is authenticated. Any other response, or any failure in the attempt to authenticate the user, will result in an exception being thrown. Access Manager then processes the exception and disallows the authentication.

Architecture

The Custom Authentication Module to be developed in this article will authenticate the credentials supplied by contacting an internet peer, and will be named SimpleSocketLoginModule. The internet peer hostname and port are supplied by the user in addition to the user's ID. If the internet peer cannot be contacted, the authentication fails, or any error occurs, SimpleSocketLoginModule throws an AuthLoginException. Otherwise, the authentication is successful.

Prerequisites

  • Access Manager installed
  • Web container installed
  • Policy Agent installed
  • Content to be protected

Step-by-Step Guide to Creating a Custom Authentication Module

Step 1 - create module properties file

Create a module description file in XML. The filename must end in ".xml", and the part before the ".xml" must correspond to the classname of the custom authentication module. Below is the module properties file for SimpleSocketLoginModule:

SimpleSocketLoginModule.xml
<?xml version='1.0' encoding="UTF-8"?>

<!DOCTYPE ModuleProperties PUBLIC "=//iPlanet//Authentication Module Properties XML Interface 1.0 DTD//EN"
          "jar://com/sun/identity/authentication/Auth_Module_Properties.dtd">

<ModuleProperties moduleName="SimpleSocketLoginModule" version="1.0" >
  <Callbacks length="3" order="1" timeout="60"
             header="In order to authenticate, you must enter your
		     username, a pass phrase, and the hostname and port of
		     the system that stores your username and
		     credentials. The hostname and port are entered as
		     hostname:port." >
    <NameCallback>
      <Prompt>
	hostname:port
      </Prompt>
    </NameCallback>
    <NameCallback>
      <Prompt>
	Username
      </Prompt>
    </NameCallback>
    <PasswordCallback echoPassword="false" >
      <Prompt>
	Pass phrase
      </Prompt>
    </PasswordCallback>
  </Callbacks>
</ModuleProperties>

Step 2 - create custom login Principal class

The first piece of Java code is the payload module:

SimpleSocketPayload.java
/**
 * Payload for the SimpleSocketLoginModule
 * transmission.
 *
 * Contributors: Terry J. Gardner
 */
package com.sun;

public class SimpleSocketPayload implements java.io.Serializable {

    /**
     * creates an instance from username, password, and success value
     * <br>
     * @param username         username to transmit
     * @param password         password to transmit
     * @param success          success value to transmit
     */
    public SimpleSocketPayload(String username,String password,boolean success) {
	this.username = username;
	this.password = password;
	this.ok = success;
    }

    /**
     * creates an instance from username, password, and false success
     * <br>
     * @param username         username to transmit
     * @param password         password to transmit
     */
    public SimpleSocketPayload(String username,String password) {
	this(username,password,false);
    }

    private String  username;
    private String  password;

    /**
     * @return         the success/failure of the transaction
     */
    public boolean isOK() {
	return ok;
    }
    private boolean ok = false;
}

Next is the class that implements the java.security.Principal interface:

SimpleSocketPrincipal.java
/*
 * SimpleSocketPrincipal.java
 *
 * Contributors: Terry J. Gardner
 */

package com.sun;

import java.security.Principal;

/**
 * Implements the methods in the Principal interface
 *
 * @author Terry J. Gardner
 */
public class SimpleSocketPrincipal implements Principal, java.io.Serializable {

    /**
     * @serial
     */
    private String name;

    public SimpleSocketPrincipal(String name) {
        if(name == null) {
            throw new NullPointerException("illegal null input");
        }
        this.name = name;
    }

    /**
     * Return the LDAP username for this <code>SimpleSocketPrincipal</code>.
     *
     * <p>
     *
     * @return the LDAP username for this <code>SimpleSocketPrincipal</code>
     */
    public String getName() {
        return this.name;
    }

    /**
     * Return a string representation of this <code>SimpleSocketPrincipal</code>.
     *
     * <p>
     *
     * @return a string representation of this <code>SimpleSocketPrincipal</code>.
     */
    public String toString() {
        return("SimpleSocketPrincipal:  " + name);
    }

    /**
     * Compares the specified Object with this <code>SimpleSocketPrincipal</code>
     * for equality.  Returns true if the given object is also a
     * <code>SimpleSocketPrincipal</code> and the two SimpleSocketPrincipals
     * have the same username. If the object to be compared is null
     * no action is taken and no exception id thrown.
     *
     * <p>
     *
     * @param o Object to be compared for equality with this
     *		<code>SimpleSocketPrincipal</code>.
     *
     * @return true if the specified Object is equal equal to this
     *		<code>SimpleSocketPrincipal</code>.
     */
    public boolean equals(Object o) {
        if(o == null) {
            return false;
        }
        if(this == o) {
            return true;
        }
        if(!(o instanceof SimpleSocketPrincipal)) {
            return false;
        }
        SimpleSocketPrincipal that = (SimpleSocketPrincipal)o;

        if(this.getName().equals(that.getName())) {
            return true;
        }
        return false;
    }

    /**
     * Return a hash code for this <code>SimpleSocketPrincipal</code>.
     *
     * <p>
     *
     * @return a hash code for this <code>SimpleSocketPrincipal</code>.
     */
    public int hashCode() {
        return name.hashCode();
    }
}

Step 3 - create custom login module class

SimpleSocketLoginModule.java
/**
 * A Sun Java System Access Manager Custom Authentication Module
 *
 * Contributors: Terry J. Gardner
 */
package com.sun;

import com.sun.identity.authentication.spi.AMLoginModule;
import com.sun.identity.authentication.spi.AuthLoginException;

import java.security.Principal;

import java.io.IOException;
import java.io.OutputStream;
import java.io.InputStream;
import java.io.ObjectOutputStream;

import java.net.Socket;
import java.net.UnknownHostException;

import java.util.Map;
import java.util.StringTokenizer;

import javax.security.auth.*;
import javax.security.auth.login.*;
import javax.security.auth.callback.*;

/**
 * @author Terry J. Gardner
 */
public class SimpleSocketLoginModule extends AMLoginModule {

    //------------ public ------------

    /**
     * initialize this object
     *
     * @param subject
     * @param sharedState
     * @param options
     */
    public void init(Subject subject, Map sharedState, Map options) {
	// no implementation necessary
    }

    /**
     * This method does the authentication of the subject
     *
     * @param callbacks           the array of callbacks from the module configuration file
     * @param state the           current state of the authentication process
     * @throws AuthLoginException if an error occurs
     */
    public int process(Callback[] callbacks,int state) throws AuthLoginException {

	// this module is married to the module properties file
	// therefore the number of callbacks must match
	if(callbacks.length < 3) {
	    throw new AuthLoginException("fatal configuration error, wrong number of callbacks");
	}

        int currentState = state;

        if(currentState == 1) {

	    // get the hostname and port
	    String hostnamePortString = ((NameCallback)callbacks[0]).getName();
	    StringTokenizer tokenizer = new StringTokenizer(hostnamePortString,":");
	    if(tokenizer.countTokens() != 2) {
		throw new AuthLoginException("Hostname:Port incorrectly formatted");
	    }
	    String hostname = tokenizer.nextToken();
            if(hostname == null || hostname.equals("")) {
                throw new AuthLoginException("hostname cannot be empty");
            }
	    int port = -1;
	    try {
		port = Integer.parseInt(tokenizer.nextToken());
	    } catch(NumberFormatException numberFormatException) {
		throw new AuthLoginException("Malformed integer in port number");
	    }
	    if(port <= 0) {
                throw new AuthLoginException("bad port number");
	    }



	    // get the username
	    userName = ((NameCallback)callbacks[1]).getName();
            if(userName == null || userName.equals("")) {
                throw new AuthLoginException("username cannot be empty");
            }


	    // get the passphrase
            String passPhrase = new String(((PasswordCallback)callbacks[2]).getPassword());
	    if(passPhrase == null) { // should not happen, but if it does ...
		passPhrase = "";
	    }



	    // contact the remote system specified in the hostname and
	    // port and send a serialized object constructed from
	    // the userName and passPhrase, read the response, if any,
	    // and proceed. For this simple example, there are no
	    // timeouts, and any exception upon socket close is
	    // ignored.
	    Socket             socket;
	    OutputStream       os;
	    InputStream        is;
	    ObjectOutputStream oos;
	    ObjectInputStream  ois;
	    try {
		socket = new Socket(hostname,port);
		is     = socket.getInputStream();
		os     = socket.getOutputStream();
		oos    = new ObjectOutputStream(os);
		ois    = new ObjectInputStream(is);
	    } catch(UnknownHostException unknownHostException) {
		throw new AuthLoginException("unknown host specified in hostname:port field");
	    } catch(IOException ioException) {
		throw new AuthLoginException("network I/O error");
	    }
	    SimpleSocketPayload payload = new SimpleSocketPayload(userName,passPhrase);
	    SimpleSocketPayload response = null;
	    try {
		oos.writeObject(payload);
	    } catch(IOException ioException2) {
		throw new AuthLoginException("network I/O error transmitting payload");
	    }
	    try {
		response = (SimpleSocketPayload)ois.readObject();
	    } catch(IOException ioException3) {
		throw new AuthLoginException("network I/O error receiving response");
	    } catch(ClassNotFoundException classNotFound) {
		throw new AuthLoginException("ClassNotFound exception receiving response");
	    }
	    try {
		if(socket != null) {
		    socket.close();
		}
	    } catch(IOException ioException4) {
		// nothing
	    }

	    // check the response from the peer
	    if(response == null) {
		throw new AuthLoginException("null response from authenticator system");
	    } else if(!response.isOK()) {
		throw new AuthLoginException("login failure");
	    }

            ++currentState;
	    // this login module only has one state, though

	    // save the user name. getPrincipal()
	    // will use the userTokenID to return the
	    // Principal object. <code>getPrincipal</code>
	    // should return the last good authentication
	    userTokenId = userName;

        }
        return -1; // -1 indicates success
    }

    /**
     * return the Principal object,

     * creating it if necessary. This method
     * is invoked at the end of successful
     * authentication session. relies on
     * userTokenID being set by process()
     *
     * <p>
     *
     * @return the Principal object or null if userTokenId is null
     */
    public java.security.Principal getPrincipal() {

        java.security.Principal thePrincipal = null;
        if(userPrincipal != null) {
            thePrincipal = userPrincipal;
        } else if(userTokenId != null) {
            userPrincipal = new SimpleSocketPrincipal(userName);
            thePrincipal = userPrincipal;
        }
        return thePrincipal;

    }

    // ------------ private ------------

    private java.security.Principal userPrincipal = null;
    private String userTokenId;
    private String userName;

}

Step 4 - install and configure in Access Manager

Create a jar file containing the SimpleSocketLoginModule.class, SimpleSocketPrincipal.class, and the SimpleSocketPayload.class files and copy the jar file to the

WEB-INF/lib

directory. Copy the SimpleSocketLoginModule.xml file to the directory containing the login modules definitions (the default is config/auth/default but this is customizable also). Restart the Access Manager.
Add the

com.sun.SimpleSocketLoginModule

to the list of pluggable authentication module classes list from the Configuration->Authentication->Core page.
Configure policy agents to use

http://accessManagerHost:port/amserver/UI/Login?module=SimpleSocketLoginModule

and restart the web container using the policy agent.

Documentation

External Links

Ancillary Files

The build.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<project name="SimpleSocketLoginModule" default="jar" basedir=".">

  <!-- properties -->
  <property name="build.dir" value="build"/>
  <property name="etc.dir" value="etc"/>
  <property name="java.dir" value="java"/>
  <property name="classes.dir" value="classes"/>
  <property name="am_services.jar"
            value="/opt/SUNWappserver/domains/domain1/applications/j2ee-modules/amserver/WEB-INF/lib/am_services.jar"/>
  <property name="am_server.d"
            value="/opt/SUNWappserver/domains/domain1/applications/j2ee-modules/amserver" />
  <property name="am_server_lib.d"
            value="${am_server.d}/WEB-INF/lib" />
  <property name="am_server_config_auth_default.d"
            value="${am_server.d}/config/auth/default" />
  <property name="simpleSocket.jar" value="SimpleSocketLoginModule.jar" />



  <!-- initializes the environment -->
  <target name="init">
    <mkdir dir="${build.dir}"/>
    <mkdir dir="${classes.dir}"/>
  </target>


  <!-- compiles java files -->
  <target name="compile" depends="init">
    <javac classpath="${am_services.jar}" srcdir="${java.dir}" destdir="${classes.dir}">
    </javac>
  </target>



  <!-- creates jar file -->
  <target name="jar" depends="compile">
    <jar destfile="${build.dir}/${simpleSocket.jar}" basedir="${java.dir}" excludes="**/*.java"/>
  </target>



  <!-- removes files and directies that ant creates -->
  <target name="clean">
    <delete dir="${classes.dir}"/>
    <delete dir="${build.dir}"/>
  </target>


  <!-- copies files to install directory -->
  <target name="install" depends="jar">
    <copy file="${build.dir}/${simpleSocket.jar}" todir="${am_server_lib.d}" />
    <copy file="${etc.dir}/SimpleSocketLoginModule.xml" todir="${am_server_config_auth_default.d}" />
  </target>



</project>

Contributors

UserEditsCommentsLabels
metadaddy 100
sidharthmishra 100
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.

Sign up or Log in to add a comment or watch this page.


The individuals who post here are part of the extended Sun Microsystems community and they might not be employed or in any way formally affiliated with Sun Microsystems. The opinions expressed here are their own, are not necessarily reviewed in advance by anyone but the individual authors, and neither Sun nor any other party necessarily agrees with them.

Copyright 1994-2009 Sun Microsystems, Inc.
Powered by Atlassian Confluence
Sun Guidelines on Public Discourse Privacy Policy Terms of Use Trademarks Site Map Employment Investor Relations Contact