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:
- Sun Java System Access Manager 7.1
- Sun Java System Application Server Platform Edition 9.0
- Netbeans
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:
<?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:
/** * 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 * * 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
/** * 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
- Download the policy agent
- Netbeans
- Java Authentication and Authorization Service (Wikipedia)
- Java Authentication and Authorization Service
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
| User | Edits | Comments | Labels |
|---|---|---|---|
| metadaddy | 1 | 0 | 0 |
| sidharthmishra | 1 | 0 | 0 |

