Ceptor Docs

Page tree
Skip to end of metadata
Go to start of metadata

A Session Controller Authentication plugin, is responsible for authenticating a user - it must look up the user information at login and populate the session with it.

An application plugin can be either a Java class, deployed in the session controller, or it can be implemented using a script, e.g. javascript.

Both types of authentication plugin have the exact same functionality, and needs to implement the same interfaces. A script based plugin has the advantage that it is easier to change at runtime, but also the disadvantage that it is harder to test, since e.g. javascript is more forgiving and much error checking is not done at compile-time, but at runtime.

Configuration

Authentication plugins are installed by adding the classname to the configuration in the property server.authenticationplugins - for scripts, you need to create a new property with the script, e.g. server.authenticationplugin.sample for the script named sample 

Then add the name sample to server.authenticationpluginscripts
e.g. server.authenticationpluginscripts=sample 

See Session Controller Properties for more info.

Plugins Supplied with Ceptor Standard Distribution

By default, the following authorization plugins are supplied:

  • dk.itp.security.ldap.jndi.ActiveDirectoryPasswordPlugin
    Uses JNDI lookups to authenticate a user in LDAP. 
  • dk.itp.security.authentication.oauth.jwt.JWTAuthenticationPlugin
    Supports OpenID Connect - both authentication/validation of JWT tokens and issuing/creating JWT tokens when acting as an Identity Provider.
  • dk.itp.security.authentication.oauth.DemoOAuth2AuthenticationPlugin
    Demonstration of OAuth2 authentication plugin - supports issuing OAuth2 tokens and authenticating using Bearer Tokens. Not suited for production use.
  • dk.itp.security.authentication.x509useradm.X509CertificatePlugin
    Supports authentication using X509 Certificates and NemID - authenticates user using challenges in the User Administration Database. Also supports document signing and verification of signatures. Supports user self-registration where user is created based upon certificate information.
  • dk.itp.security.authentication.x509useradm.SSLCertificatePlugin
    Supports authentication and document signing using X509 certificates as client certificates - supports e.g. certificates stored in smartcards.
  • dk.itp.portalprotect.wss.WSSAuthenticationPlugin
    Supports authenticatiion using WS-Security, XML signatures. Also supports SOAP and XML signing/encryption, verification and decryption. See WebService Security andWS-Security Properties for more information.
  • dk.itp.security.authentication.x509.X509CertificatePlugin
    Authenticates user using only information provided in the certificate - supports both SSL Client and NemID certificates and authentication. Does not use a backend store, but only uses list of trusted issuers.
    This can be used to authenticate using certificates / nemid if you have no user store and accept all certificates issued by the configured issuer.
  • dk.itp.security.ldap.jndi.ActiveDirectoryPasswordPlugin
    Authenticates user using userid/password against Active Directory.
  • dk.itp.portalprotect.saml.ADFSSamlSSOAuthPlugin
    Supports SAML ADFS / WebSSO authentication and federation - can both consume authentications and issue tokens. 
  • dk.itp.security.authentication.CrossDomainAuthenticationPlugin
    Supports Crossdomain Session Sharing 
  • dk.itp.security.passticket.server.DummyLogin
    Dummy authentication plugin used solely for testing - does not verify credentials but always accepts authentication with the given credentials.
  • dk.itp.portalprotect.useradmin.server.EmailUAAuthenticationPlugin
    Supports creating users, generating OTP codes and sending them out to them via email - uses UserAdmin API to create users in database.
    Supports authenticating users with the codes they received, letting them choose a new password and updates the users.
  • dk.itp.security.authentication.eticket.plugins.ETicketAuthenticationPlugin
    Provides ETicket support for authenticating users - used in some enterprise SSO scenarios - was previously used in NetID before NemID.
  • dk.itp.security.passticket.server.FileLogin
    Authenticates user using userid/password - uses property files as authentication store.
  • dk.itp.portalprotect.useradmin.server.GoogleAuthUAAuthenticationPlugin (see TOTP (Google) Authenticator)
    Uses Google Authenticator time-based OTP codes for authenticating users - supports generating new tokens and QR codes for use when registering users.
    Uses UserAdmin API for user repository. 
  • dk.itp.security.authentication.jaas.JAASAuthenticationPlugin
    Supports bridging to JAAS LoginModules for authentication.
  • dk.itp.security.ldap.LoginHandlerLdapImpl
    Supports authenticating against an LDAP server using Netscape client API (which performs much better than JNDI).
  • dk.itp.portalprotect.saml.NemIDSamlSSOAuthPlugin
    Supports authenticating users using NemID SAML SSO - this can be used by priviledged service providers for single signon with banks using NemID.
  • dk.itp.security.authentication.ntlm.NTLMAuthenticationPlugin
    Works together with Ceptor Gateway / Dispatcher to authenticate users using NTLM against Active Directory Server in an intranet environment.
  • dk.itp.security.authentication.oauth.LiveConnectAuthenticationPlugin
    Outdated plugin which authenticates users using LiveConnect - you should use OpenID Connect or ADFS/WebSSO plugins instead of this.
  • dk.itp.portalprotect.useradmin.server.SMSUAAuthenticationPlugin (see SMS / Text OTP )
    Generates an OTP code and sends it via SMS using one of the support SMS providers - lets user enter code as two-factor authentication. Uses UserAdmin API against database to retrieve user information and phone number.
  • dk.itp.security.authentication.spnego.SpnegoAuthenticationPlugin
    Authenticates a user using SPNEGO / Kerberos in an intranet environment.
  • dk.itp.portalprotect.useradmin.server.U2FUAAuthenticationPlugin
    Supports Fido U2F Hardware tokens for two-factor authentication - supports user registration and authentication. Uses UserAdmin API to access user repository.
  • dk.itp.security.authentication.bankid.se.BankIDSEAuthenticationPlugin
    Allows authentication using the swedish BankID service.
  • dk.itp.portalprotect.useradmin.server.UAAuthenticationPlugin
    Supports userid/password authentication against UserAdmin API database.
  • dk.itp.security.authentication.ltpa.LTPAAuthenticationPlugin
    Supports authenticating using LTPA Tokens, and also generation of new LTPA tokens based upon currently authenticated user.
  • dk.itp.security.server.NoOpAuthenticationPlugin
    This plugin does not authenticate a user, but allows creating an empty session from a ticket, essentially giving two keys to the session - the regular ID and the ticket, but without using it for any type of authentication or deriving any value from it.

Besides the plugins listed above, we have many more customer specific authentication plugins available or abstract plugins which you can extend and use for your own authentication plugins - please contact us for more information if required.

Developing an Authentication Plugin


An authentication plugin must implement the interface dk.itp.security.authentication.IAuthenticationPlugin and it must inherit from the abstract class dk.itp.security.authentication.DefaultAuthenticationPlugin

Below is the IAuthenticationPlugin interface listed:

package dk.itp.security.authentication;
import java.util.Properties;

import dk.itp.security.passticket.PTException;
import dk.itp.security.passticket.User;
import dk.itp.security.passticket.server.PTSServer;
import dk.itp.statistics.Statistics;
/**
 * This interface must be implemented by all authentication plugins, it combines the common characteristics for
 * all types of authentication.
 *
 * @author Kim Rasmussen
 * 
 * <pre>
 * PortalProtect - Security infrastructure
 * Copyright(c) 2001, IT Practice A/S, All rights reserved.
 * 
 * This source code is confidential.
 * </pre>
 */
public interface IAuthenticationPlugin {
	/**
	 * Tries to change the users password. Note that this does not work for all types of authentication
	 * e.g. CBT has a local password which is never seen by the server, so it requires change locally
	 * instead of at the server.
	 */
	void changePassword(User user, String oldPassword, String newPassword) throws PTException;

	/**
	 * This method checks user confirmations - a confirmation can be a digital signature, if the authentication method
	 * supports it, but it can also be a simple re-authentication, e.g. in case of userid/password authentication.
	 *
	 * Digital signatures is ofcourse preferred, since they offer improved security
	 *
	 */
	void confirm(User user, String signtext, Object credentials) throws PTException;

	/**
	 * Returns the authentication level, the higher the level, the more secure this authentication is considered to be.
	 * @return String null if no information is available
	 */
	int getAuthenticationLevel();

	/**
	 * Returns the type of authentication, all plugins have assigned a particular id identifying that specify type
	 * of authentication.
	 */
	int getAuthenticationType();

	/**
	 * Returns the name of this particular authentication method
	 */
	String getName();

	/**
	 * Try to login the specified user session, using the supplied userid and credentials.
	 */
	void login(User user, String userid, Object credentials) throws PTException;

	/**
	 * Try to validate the specified user session, using the supplied userid and credentials.
	 */
	void validate(User user, String userid, Object credentials) throws PTException;

	/**
	 * Try to login the specified user session, using the supplied userid and credentials.
	 */
	void login(User user, String userid, Object credentials, Object newPassword) throws PTException;

    /**
     * Return a authentication token based on the specified user. Token can be used for SSO to other systems
     * @param user
     * @return cryptographic SSO token. If the token is binary, return Base64 encoding of the binary token
     * @throws PTException
     */
    String newToken(User user) throws PTException;

    /**
     * Return a authentication token based on the specified user. Token can be used for SSO to other systems
     * @param user
     * @param inputToken 
     * @return cryptographic SSO token. If the token is binary, return Base64 encoding of the binary token
     * @throws PTException
     */
    String newToken(User user, String inputToken) throws PTException;

	/**
	 * Notifies the plugin that the users state will be changed, this gives it the oppertunity to save the state
	 * to a database, so it is persistent.
	 */
	void modifyState(User user, String key, String value);

	/**
	 * Notifies the plugin that the user has logged off
	 */
	void logoff(User user, boolean timeout);

	/**
	 * Updates the configuration for this plugin
	 */
	void setConfiguration(Properties props);

	/**
	 * Gives the plugin a chance to log an entry to the transaction log, if it has any
	 */
	void logToTransactionLog(User user, String context, String text, boolean signed);

	/**
	 * Return detailed html formattet information about plugin's information and/or current status.
	 * @return String null if no information is available
	 */
	String getStatusText();
	
	/**
	 * Return detailed html formattet information about plugin's information and/or current status.
	 * @param action Action supplied by the plugin previously - allows the plugin to perform actions
	 * @return String null if no information is available
	 */
	String getStatusText(String action);

	/**
	 * Gives the plugin a place to put its statistics, if any
	 */
	void setStatistics(Statistics stat);
	
	/**
	 * Gives the plugin a list of allowed environment IDs - only access to the plugin from a session in a listed
	 * environment is allowed.
	 * 
	 * @param ids List of environment IDs that are allowed.
	 */
	void setAllowedEnvironmentIDs(int[] ids);
	
	/**
	 * Query the list of allowed environment IDs
	 * @return Array of IDs, or null if no restrictions
	 */
	int[] getAllowedEnvironmentIDs();
	
	/**
	 * Checks if access to this plugin from a given environment ID is allowed
	 * @param id Environment ID to check against
	 * @return True if access is allowed, false if not
	 */
	boolean isEnvironmentAllowed(int id);
	
    /**
     * Gives the plugin a list of allowed segment IDs - only access to the plugin from a session in a listed
     * segment is allowed.
     * 
     * @param ids List of environment IDs that are allowed.
     */
    void setAllowedSegmentIDs(int[] ids);
    
    /**
     * Query the list of allowed segment IDs
     * @return Array of IDs, or null if no restrictions
     */
    int[] getAllowedSegmentIDs();
    
    /**
     * Checks if access to this plugin from a given segment ID is allowed
     * @param id Segment ID to check against
     * @return True if access is allowed, false if not
     */
    boolean isSegmentAllowed(int id);
    
	/**
	 * Called after stop() when the session controller is started again from the admin GUI.
	 * Note that a plugin cannot depend on start being called initially, it should only expect it to happen after stop
	 * has been called. 
	 */
	void start();

	/**
	 * Called if the administrator requested that the session controller should stop - gives the plugin a chance
	 * to do some cleanup, and close connections.
	 */
	void stop();

    /**
     * Called when the plugin is started, to give it the session controller instance - it can use it for doing advanced
     * stuff, such as sending commands to other session controllers if needed.
     * 
     * @param sessionController
     */
    void initialize(PTSServer sessionController);
    
    /**
     * Called when the plugin is about to be uninstalled/unloaded - it will give it a chance to unregister any listeners.
     */
    void uninstall();
    
    /**
     * Let the authentication plugin execute an arbitrary command - this is used to extend the plugin with new
     * functionality, such as letting it decrypt signed SOAP messages or anything that is in its nature plugin specific
     * which we do not want to create different methods for that all plugins would then have to implement.
     * 
     * @param user Session
     * @param input Input object, can be anything serializable
     * @return Whatever object the command returns
     */
    Object executeCommand(User user, String name, Object input) throws PTException;
    
	/**
	 * Allows the plugin to respond to ping requests - ie. it can be called by an application who wishes to query
	 * the state of all authentication plugins and their backend databases.
	 * The plugin can add any properties it wishes to the properties object it gets called with - these properties
	 * will be returned to the requesting application. 
	 *  
	 * @param props read/write list of properties the plugin can use to report its detailed status, such as connections to backend servers/databases.
	 * @return true if the plugin considers itself functional and if all backend server connections are ok - false otherwise.
	 */
    boolean ping(Properties props);
    
    /**
     * Called when a persisted session (where the user is identified using this plugin) is restored so it can give
     * the plugin a chance to update its backend servers with the new session
     * 
     * @param sessionID Session ID of user who is doing the restore
     * @param user Session of user who is doing the restore
     * @param restoredSession The restored session
     * @throws PTException
     */
    void persistedSessionRestored(String sessionID, User user, User restoredSession) throws PTException;
    
    /**
     * Called to give the plugin a chance to reload its private keys. Called if Luna security provider is reloaded so
     * the private keys need to be reloaded from the JCE provider.
     */
    void reloadPrivateKeys();
    
    /**
     * A new user has just been created based upon a ticket, e.g. an OAuth ticket - so the plugin can fill the session with whatever
     * information it can obtain from the ticket.
     * 
     * @param user
     * @throws PTException
     */
    void createFromTicket(User user) throws PTException;
    
}


The DefaultAuthenticationPlugin implements many of these methods, so you do not need to implement all of them in your plugin.

User / Session Object

The dk.itp.security.passticket.User object you see referenced a lot above, contains the shared session.

In an abbreviated form (method content and serialization methods removed for clarity) it looks like this:

package dk.itp.security.passticket;
import java.io.DataOutputStream;
import java.io.Externalizable;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Vector;

import dk.itp.peer2peer.AbstractPeer;
import dk.itp.peer2peer.Peer;
import dk.itp.security.utils.RFC822Date;

/**
 * Contains all known data about a user/session that is currently in use.
 *
 * @author Kim Rasmussen
 * 
 * <pre>
 * PortalProtect - Security infrastructure
 * Copyright(c) 2001, IT Practice A/S, All rights reserved.
 * 
 * This source code is confidential.
 * </pre>
 */
public class User implements Externalizable, Cloneable {
	public volatile long lastAccessed;

	public volatile String sessionID;

	public volatile String password;
	public volatile String userid;
	public volatile boolean isLoggedOn;
	public volatile Vector aclList;

	public volatile String customerID;
	public volatile String agreementID;
	public volatile boolean isInternalUser;

	public volatile String cicsUser;
	public volatile String cicsPassword;

	public volatile String username;

	public volatile long modifySerial;
	public volatile long lastModified;
	public volatile Hashtable<String, String> stateVariables;

	public volatile int authenticationMethod;
	public volatile int authenticationLevel;

	public volatile Vector<String> sessionHistory;

	public volatile String userGroups[];
	
	public volatile Hashtable<String, Object> stateObjects;
	
	/** Session controller which owns this session */
	public volatile String owner;
	
	/** Name of agent who created this session, if available */
	public String creatingAgent;  
	
	// If nonzero, this is the time the session will automatically be logged off if it is not accessed before
	public volatile long delayedLogoffAt;

	/** OAuth or similar ticket this session was created with - must not be changed after creation */
	public String ticket;
	
	/**
	 * Used by sessioncontroller to track which agents have an interest in this session - note that this is
	 * transient, and only kept on the server.
	 */
	public transient ArrayList interestedAgents;
	public transient HashMap agentNotifyCount;

	/**
	 * Creates a deep clone of the user
	 */
	public Object clone();
	
	/**
	 * Copies all content of this session to the supplied user
	 */
	public void copyTo(User usr);

	/**
	 * Adds a history text to this session - the session history can be used for debugging to determine what
	 * has occurred with the particular session during its lifetime.
	 */
	public synchronized void addHistory(String text);

	public synchronized Vector getHistory();

	/**
	 * This method returns a list of groups that the user is a member of
	 */
	public String[] getGroups();

	/** 
	 * This method allows to change the groups that this user is a member of
	 */
	public void setGroups(String groups[]);
	public void setStateObject(String key, Serializable value);
	
	public Object getStateObject(String key);

	public void setStateVariable(String key, String value);
	public String getStateVariable(String key);
	
	public Object removeStateObject(String key);
	public String removeStateVariable(String key);
}

Explanation of Session Contents

Following is what should be considered essential information about the session / User object.

First, it is important to notice that nobody except for the session controller ever changes the contents of a session. An agent might ask for it to happen, but it cannot do it itself. A change only happens after ensuring that all caches are cleared of this session across the cluster to be certain that no old copy exists that might contain outdated data.

Internals for housekeeping

First, it has a number of fields, which are mainly used by the session controller for clustering and housekeeping.

These are:

  • lastAccessed
    Used for idle timeout - idle timeout is not an exact science, but variable - an agent may cache the session and use the cached copy for up to 5 minutes, this means that the sessioncontroller might not know a session has been touched for up to 5 minutes, meaning if the timeout is set to 30 minutes, the session might be accessed without being modified for up to 5 minutes, so the actual user experienced timeout may be as low as 25 minutes. 
  • modifySerial / lastModified
    These are used to keep track of when the session was modified last - useful amongst a cluster, in case connection between cluster members was split and later reestablished (the split brain syndrom) - these variables are used to figure out which copy is the latest. 
  • owner
    The sessioncontroller who currently owns the session - only the owner can timeout sessions - if the owner disconnects for some reason, the other session controllers will figure it out and take ownership themselves after a while. This is done to avoid multiple sessioncontrollers timing out the session and notifying everyone about it simultaneously - in most cases it wouldn't matter, but in some setups other systems piggyback upon the sessions created by the sessioncontroller and depend on getting notified properly when they are cleaned up. Multiple notifications would cause problems for such systems.
  • interestedAgents
    These are the agents connected to this session controller who have expressed an interest in this session earlier - used to figure out who to notify in case it is updated. 
  • agentNotifyCount
    For each agent, the number of times it has been notified about session changes - after a certain limit, notifications start getting asynchronous, since the agent is sure not have a cache of the session so we need not wait on it to clear it. 
  • history
    Contains the history of the session, e.g. when someone logged in, logged out etc. - used only for diagnostics.

Regular fields/attributes

These are the regular commonly used attributes in the session.

  • sessionID
    Primary unique key for the session.
  • userid
    Userid of the currently authenticated user. 
  • password
    Password of currently authenticated user - often not present, since password might be hidden - up the authentication plugin if it wants to keep it there. It might be needed for using e.g. NTLM or SPNEGO between the dispatcher/gateway and application server. 
  • username
    Name of user.
  • isLoggedOn
    true if user is authenticated, false if not - note that userid, username etc. might contain values even if isLoggedOn is not, so always check isLoggedOn - do not ever check if there is a userid in the session, and assume the user is authenticated if there is. Common scenarios where a userid is present, could be that anonymous users are enabled, or a multi-factor login is in progress but not completed yet. 
  • customerID
    Customer ID - with UserAdmin API as a user repository, this contains the userid (primary key) from the database identifying the user - it could be used for anything and it is up to the authentication plugin to define what. 
  • agreementID
    Often you have an agreement number for users, if so this is a good place to store it - Ceptor itself seldomly use it, it is mostly for customer specific cases. 
  • isInternalUser
    If you have both internal and external users, set this to true for internal ones - allows your applications to differ between them.
  • userGroups
    Contains a list of usergroups / roles the user is a member of. Note that in some setups, this is not known at time of authentication and an authorization plugin might instead store on the groups information about which users is member of it - that method scales badly so if at all possible, try to resolve usergroups at login, and only at login.
  • authenticationMethod
    Each installed authentication plugin has a unique ID - this is the ID of the plugin which authenticated the user. 
  • authenticationLevel
    Level of trust that the user is who he claims to be - the higher the level, the more secure the authentication is - it might depend on the authentication method, but it could also depend on external means - e.g. have we verified user by having an employee look him in the eye? Have we sent a pin-letter to his physical address etc. Usually set to a lower value for userid/password and a higher value for multifactor or biometric authentication. 
  • stateVariables
    name/value pairs of additional properties stored in the session - intentionally kept as strings. Agents or authentication plugins can store anything in here, from hair color and shoe size to address, email and phone information.
    Think carefully about what you place here - do not just throw lots of JSON objects or similar into the session, since it will affect memory usage and performance when session content is transferred between components. 
  • ticket
    If the session is a "pseudo" session created from a ticket or a token, such as a JWT bearer token (or potentially from an SSL certificate) then this contains the ticket/token. Agents can access a session using either the session ID or ticket as key. 
  • aclList
    Access Control Lists - not commonly used, but can express non-group information about what the user has access to.
  • stateObjects
    Can contain serialized objects - meant primarily for internal use - used by dispatcher/gateway for things like stored cookies, stickiness etc. In general an authentication plugin should not use it, and an agent certainly shouldn't. But in some cases a string is not enough.
  • delayedLogoffAt
    If delayed logoff is activated, this is the time when it will be trigged - a rarely used feature, but allows so-called delayed logoff that is cancelled if the session is accessed again before delayed logoff kicks in. Can be used together with Crossdomain Session Sharing for logging the user off when he leaves the page, but cancelling the logoff when he gets active on the new site. A delayed logoff is typically triggered after less than 5 seconds.

Sample Authentication Plugins

Here are a few example plugins:

DummyLogin
package dk.itp.security.passticket.server;
import java.util.Vector;

import dk.itp.security.authentication.DefaultAuthenticationPlugin;
import dk.itp.security.passticket.PTException;
import dk.itp.security.passticket.User;

/**
 * Copyright (c) IT Practice A/S
 *
 * Dummy login handler, which accepts all userids and passwords without validating them.
 * This should only be used for testing, since in production we want to validate the user
 * with some real information.
 *
 * @author Kim Rasmussen
 * 
 * <pre>
 * PortalProtect - Security infrastructure
 * Copyright(c) 2001, IT Practice A/S, All rights reserved.
 * 
 * This source code is confidential.
 * </pre>
 */
public class DummyLogin extends DefaultAuthenticationPlugin {
	/**
	 * Try to change the userid and password for this user
	 */
	public void changePassword(User user, java.lang.String oldPassword, java.lang.String newPassword) {
		if (user.isLoggedOn && oldPassword.equals(user.password)) {
			user.password = newPassword;
		}
	}

	/**
	 * This method checks user confirmations - a confirmation can be a digital signature, if the authentication method
	 * supports it, but it can also be a simple re-authentication, e.g. in case of userid/password authentication.
	 *
	 * Digital signatures is ofcourse preferred, since they offer improved security
	 *
	 */
	public void confirm(dk.itp.security.passticket.User user, java.lang.String signtext, java.lang.Object confirmObject)
		throws dk.itp.security.passticket.PTException {
		throw new PTException("Confirmation not supported by this authentication plugin");
	}

	/**
	 * Returns the authentication level, the higher the level, the more secure this authentication is considered to be.
	 */
	public int getAuthenticationLevel() {
		return 0;
	}

	/**
	 * Returns the type of authentication, all plugins have assigned a particular id identifying that specify type
	 * of authentication.
	 */
	public int getAuthenticationType() {
		return dk.itp.security.passticket.AuthTypes.AUTHTYPE_DUMMY;
	}

	/**
	 * Returns the name of this particular authentication method
	 */
	public String getName() {
		return "Dummy userid/password login";
	}

	/**
	 * Try to login the specified user session, using the supplied userid and password.
	 */
	public void login(User user, String userid, Object credentials) {
		login(user, userid, credentials, null);
	}

	/**
	 * Try to login the specified user session, using the supplied userid and password.
	 */
	public void login(User user, String userid, Object credentials, Object newPassword) {
		user.isLoggedOn = true;
		user.userid = userid;
		user.password = credentials.toString();
		user.isInternalUser = false;
		user.customerID = userid;

		user.aclList = new Vector();
		user.aclList.addElement("01234567.00");
		user.aclList.addElement("12345678.00");
		user.aclList.addElement("EBXX3006.00");
	}

	/**
	 * Sets the configuration - the login handler can read from the properties to determine its own configuration.
	 */
	public void setConfiguration(java.util.Properties props) {}

	public void modifyState(User user, String key, String value) {}
}


Here is an X509 Certificate validation plugin, which does not use a datastore - it is part of the samples distributed with Ceptor PortalProtect.

package dk.portalprotect.nodb.plugins;

import java.util.Hashtable;
import java.util.Properties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import dk.itp.security.authentication.x509.AbstractX509CertificatePlugin;
import dk.itp.security.certificate.Subject;
import dk.itp.security.passticket.PTException;
import dk.itp.security.passticket.User;
import dk.itp.security.passticket.server.AuthErrorCodes;
import dk.itp.security.utils.Base64;

/**
 * X509 certificate plugin that uses useradmin database
 *
 * @author Kim Rasmussen
 * 
 * <pre>
 * PortalProtect - Security infrastructure
 * Copyright(c) 2001, IT Practice A/S, All rights reserved.
 * 
 * This source code is confidential.
 * </pre>
 */
public class X509CertificatePlugin extends AbstractX509CertificatePlugin {
	private Logger log = LoggerFactory.getLogger(X509CertificatePlugin.class);
	/**
	 * @see dk.itp.security.authentication.IAuthenticationPlugin#getName()
	 */
	public String getName() {
		return "X509 Certificate plugin";
	}

	public void confirm(User user, String signtext, Object credentials) throws PTException {
		try {
//			Subject subject = verifyCertificate(user, credentials.toString(), false, false, null, signtext);
			verifyCertificate(user, credentials.toString(), false, false, null, signtext);
			
			// Here, we could use subject if we want to check if the signer is the same as the one who authenticated
			
				
		} catch(PTException e) {
			throw e;
		} catch (Throwable t) {
			log.error("Got exception trying to  verify digital signature", t);
			throw new PTException("Unknown error occurred", AuthErrorCodes.ERROR_GENERALERROR, t.toString());
		}
	}

	/**
	 * logon with x.509 certificate.
	 * <ol>
	 * <li>Check RFC 3275 message integrity
	 * <lI>Check certificate from message for expiration
	 * <li>Check certificate against revocation-list 
	 * <li>Check certificate against CA issuer certificate.
	 * <li>Check certificate PID and CPR against CA PID-mapping database (if CA has implemented one).
	 * </ol>
	 * 1) verify integrity of RFC 3275 message
	 * 2) 
	 */
	public void login(User user, String userid, Object credentials) throws PTException {
		try {
			Subject subject;
			String xmldsig;
			
			// If credentials is a single string, we expect it to be the xmldsig from the applet
			if (credentials instanceof String) {
				xmldsig = (String)credentials;
				subject = verifyCertificate(user, xmldsig, false, false, null, null);
			} else if (credentials instanceof Object[]){
				// If credentials is an array, we expect it to be 2 elements, the first is the xmldsig, the second is the CPR number
				xmldsig = (String) ((Object[])credentials)[0];
				String cpr = (String) ((Object[])credentials)[1];
				subject = verifyCertificate(user, xmldsig, false, true, cpr, null);
				
				user.stateVariables.put("cpr", cpr);
			} else if (credentials == null) {
				throw new PTException("Credentials cannot be null");
			} else {
				throw new PTException("Unrecognized credentials of type: " + credentials.getClass().getName());
			}
			
			userid = subject.getSerialNumber();
			
			if (user.stateVariables == null)
				user.stateVariables = new Hashtable();
			user.stateVariables.put("company", subject.getO());
			user.stateVariables.put("country", subject.getC());
			user.stateVariables.put("full_subject", subject.getOrderedSubjectDN());
				
			String serial = userid;

			if (serial.startsWith("PID")) {
				user.stateVariables.put("pid", serial.substring(4));
			} else if (serial.startsWith("CVR")) {
				String cvr = serial.substring(4);
				if (cvr.indexOf('-') > 0)
					cvr = cvr.substring(0, cvr.indexOf('-'));
				user.stateVariables.put("cvr", cvr);

				int idx = serial.indexOf("-UID:");
				if (idx < 0) {
					idx = serial.indexOf("-RID:");
					if (idx >= 0) {
						// Medarbejder cert
						user.stateVariables.put("rid", serial.substring(idx + 5));
					}

				} else {
					// Virksomheds cert
					user.stateVariables.put("uid", serial.substring(idx + 5));
				}
			}

			if (Base64.isBase64(xmldsig))
				xmldsig = Base64.decode(xmldsig);
			
			user.stateVariables.put("xmldsig", xmldsig);
			user.customerID = userid;
			user.userid = userid;
			user.username = subject.getCn();
			
			// Here, we can have additional logic to load data from a backend system, and e.g. if we want the logon to fail
			// if we do not know the users CPR number, then we can throw an exception that signals the logon app that we
			// need the CPR number - it can then later call logon with both the saved xmldsig and the CPR number and we
			// can verify that they match.
			
			// throw new PTException("Need CPR", AuthErrorCodes.ERROR_UNDER_CREATION, "Need CPR number");
			
			user.isLoggedOn = true;
		} catch(PTException e) {
			throw e;
		} catch (Throwable t) {
			log.error("Got exception trying to login", t);
			throw new PTException("unexpected error occurred", AuthErrorCodes.ERROR_GENERALERROR, t.toString());
		}
	}

	/**
	 * Cleanup when user is logging off, by removing all state variables and state objects.
	 * Note that by removing stateobjects, we risk interfering with internal PP state, but in this case we want to
	 * make sure we delete any references to old http cookies that might be stored by the dispatcher when the user
	 * logs off to ensure that the http session is also removed towards the application servers.
	 */
	public void logoff(User user, boolean timeout) {
		if (user.stateVariables != null)
			user.stateVariables.clear();
		if (user.stateObjects != null)
			user.stateObjects.clear();
	}
	
	public void setConfiguration(Properties props) {
		super.setConfiguration(props);		
	}
}


Here is an example of an authentication plugin, which uses a simple database to store the relation between a PID and a CPR for NemID.

package dk.portalprotect.simpledb.plugins;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Hashtable;
import java.util.Properties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dk.itp.security.authentication.x509.AbstractX509CertificatePlugin;
import dk.itp.security.certificate.Subject;
import dk.itp.security.passticket.PTException;
import dk.itp.security.passticket.User;
import dk.itp.security.passticket.server.AuthErrorCodes;
import dk.itp.security.utils.Base64;
import dk.itp.security.utils.jdbc.DBPool;
import dk.itp.statistics.Statistics;

/**
 * X509 certificate plugin that uses useradmin database
 *
 * @author Kim Rasmussen
 * 
 * <pre>
 * PortalProtect - Security infrastructure
 * Copyright(c) 2001, IT Practice A/S, All rights reserved.
 * 
 * This source code is confidential.
 * </pre>
 */
public class X509CertificatePlugin extends AbstractX509CertificatePlugin {
	private Logger log = LoggerFactory.getLogger(X509CertificatePlugin.class);
	
	DBPool dbPool = DBPool.getPool("simpledb");
	
	/**
	 * @see dk.itp.security.authentication.IAuthenticationPlugin#getName()
	 */
	public String getName() {
		return "X509 Certificate plugin";
	}

	public void confirm(User user, String signtext, Object credentials) throws PTException {
		try {
//			Subject subject = verifyCertificate(user, credentials.toString(), false, false, null, signtext);
			verifyCertificate(user, credentials.toString(), false, false, null, signtext);
			
			// Here, we could use subject if we want to check if the signer is the same as the one who authenticated
			
				
		} catch(PTException e) {
			throw e;
		} catch (Throwable t) {
			log.error("Got exception trying to  verify digital signature", t);
			throw new PTException("Unknown error occurred", AuthErrorCodes.ERROR_GENERALERROR, t.toString());
		}
	}

	/**
	 * logon with x.509 certificate.
	 * <ol>
	 * <li>Check RFC 3275 message integrity
	 * <lI>Check certificate from message for expiration
	 * <li>Check certificate against revocation-list 
	 * <li>Check certificate against CA issuer certificate.
	 * <li>Check certificate PID and CPR against CA PID-mapping database (if CA has implemented one).
	 * </ol>
	 * 1) verify integrity of RFC 3275 message
	 * 2) 
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public void login(User user, String userid, Object credentials) throws PTException {
		try {
			Subject subject;
			String xmldsig;
			
			if (user.stateVariables == null)
				user.stateVariables = new Hashtable();
			
			// If credentials is a single string, we expect it to be the xmldsig from the applet
			if (credentials instanceof String) {
				xmldsig = (String)credentials;
				subject = verifyCertificate(user, xmldsig, false, false, null, null);
			} else if (credentials instanceof Object[]){
				// If credentials is an array, we expect it to be 2 elements, the first is the xmldsig, the second is the CPR number
				xmldsig = (String) ((Object[])credentials)[0];
				String cpr = (String) ((Object[])credentials)[1];
				subject = verifyCertificate(user, xmldsig, false, true, cpr, null);
				
				user.stateVariables.put("cpr", cpr);
				
				storeCpr(subject.getSerialNumber(), cpr);
			} else if (credentials == null) {
				throw new PTException("Credentials cannot be null");
			} else {
				throw new PTException("Unrecognized credentials of type: " + credentials.getClass().getName());
			}
			
			userid = subject.getSerialNumber();
			
			user.stateVariables.put("company", subject.getO());
			user.stateVariables.put("country", subject.getC());
			user.stateVariables.put("full_subject", subject.getOrderedSubjectDN());
				
			String serial = userid;

			if (serial.startsWith("PID")) {
				user.stateVariables.put("pid", serial.substring(4));
			} else if (serial.startsWith("CVR")) {
				String cvr = serial.substring(4);
				if (cvr.indexOf('-') > 0)
					cvr = cvr.substring(0, cvr.indexOf('-'));
				user.stateVariables.put("cvr", cvr);

				int idx = serial.indexOf("-UID:");
				if (idx < 0) {
					idx = serial.indexOf("-RID:");
					if (idx >= 0) {
						// Medarbejder cert
						user.stateVariables.put("rid", serial.substring(idx + 5));
					}

				} else {
					// Virksomheds cert
					user.stateVariables.put("uid", serial.substring(idx + 5));
				}
			}

			if (Base64.isBase64(xmldsig))
				xmldsig = Base64.decode(xmldsig);
			
			user.stateVariables.put("xmldsig", xmldsig);
			user.customerID = userid;
			user.userid = userid;
			user.username = subject.getCn();
			
			// Here, we can have additional logic to load data from a backend system if needed

			// If we are logging in using a POCES certificate, lookup the CPR - if we do not know it, fail.
			if (serial.startsWith("PID")) {
				// Lookup the CPR
				String cpr = lookupCpr(serial);
				if (cpr == null) {
					throw new PTException("Need CPR", AuthErrorCodes.ERROR_UNDER_CREATION, "Need CPR number");					
				} else {
					user.stateVariables.put("cpr", cpr);					
				}
			}
			
			user.isLoggedOn = true;
		} catch(PTException e) {
			throw e;
		} catch (Throwable t) {
			log.error("Got exception trying to login", t);
			throw new PTException("unexpected error occurred", AuthErrorCodes.ERROR_GENERALERROR, t.toString());
		}
	}

	/**
	 * Cleanup when user is logging off, by removing all state variables and state objects.
	 * Note that by removing stateobjects, we risk interfering with internal PP state, but in this case we want to
	 * make sure we delete any references to old http cookies that might be stored by the dispatcher when the user
	 * logs off to ensure that the http session is also removed towards the application servers.
	 */
	public void logoff(User user, boolean timeout) {
		if (user.stateVariables != null)
			user.stateVariables.clear();
		if (user.stateObjects != null)
			user.stateObjects.clear();
	}
	
	
	@Override
	public void setStatistics(Statistics stat) {
		super.setStatistics(stat);
		dbPool.setStatistics(stat);
	}
	
	public void setConfiguration(Properties props) {
		super.setConfiguration(props);
				
		try {
			dbPool.initialize(props);
		} catch (SQLException e) {
			log.error("Problem initializing database", e);
		}
	}
	
	private void storeCpr(String pid, String cpr) throws SQLException {
		Connection conn = dbPool.getConnection();
		try {
			PreparedStatement stmt = dbPool.prepareStatement(conn, "insert into PIDCPR (PID, CPR) values(?, ?)");
			stmt.setString(1, pid);
			stmt.setString(2, cpr);
			stmt.executeUpdate();
		} finally {
			dbPool.releaseConnection(conn);
		}
	}
	
	private String lookupCpr(String pid) throws SQLException {
		Connection conn = dbPool.getConnection();
		try {
			PreparedStatement stmt = dbPool.prepareStatement(conn, "select CPR from PIDCPR where PID=?");
			stmt.setString(1, pid);
			ResultSet result = stmt.executeQuery();
			
			if (result.next()) {
				return result.getString(1);
			}
			return null;
		} finally {
			dbPool.releaseConnection(conn);
		}
	}
}


... and a demonstration OAuth2 plugin

package dk.portalprotect.sample.oauth2;

import java.io.IOException;
import java.util.Hashtable;
import java.util.Properties;
import java.util.UUID;

import dk.itp.caching.lru.LruCache;
import dk.itp.security.authentication.oauth.AbstractOAuth2AuthenticationPlugin;
import dk.itp.security.passticket.PTException;
import dk.itp.security.passticket.User;
import dk.itp.security.passticket.server.AuthErrorCodes;
import dk.itp.security.utils.Base64;
import dk.portalprotect.oauth.OAuth2Helper;

/**
 * Demonstration OAuth2 plugin - not suitable for production use.
 * It issues a new ticket by generating a random key and returning it as the grantticket. It then persists the issuing
 * users ticket and saves it in a memory cache for up to an hour - during that our, the grant ticket gets access to the
 * previously saved session contents.
 * 
 * Please note that a production OAuth plugin must take care to validate the provided client_id and ensure that it
 * already exists in a known registry - it needs to ensure that the client id is valid for the attempted operation, 
 * especially it needs to ensure that implicity grant is only used if explicitly allowed.
 * 
 * Also, the provided scope must be validated for content which has meaning to the application, and the application
 * needs to use this information when authorizing a request and allowing access to data on the users behalf.
 *  
 * @author Kim Rasmussen
 * @version $Revision$
 *
 * <pre>
 * PortalProtect - Security infrastructure
 * Copyright(c) 2014, Asseco Denmark A/S, All rights reserved.
 * 
 * This source code is confidential.
 * </pre>
 */
public class DemoOAuth2AuthenticationPlugin extends AbstractOAuth2AuthenticationPlugin {
	private LruCache cache = new LruCache();
	private LruCache authCache = new LruCache();
	
	private class AuthCacheEntry {
		String bearertoken;
		String clientid;
		String clientSecret = "password";
		String redirectUri;
	}
	
	public DemoOAuth2AuthenticationPlugin() {
		cache.setCacheSize(1000);
		cache.setTimeout(60000*60); // 1 hour
		cache.setForceTimeout(60000*60); // 1 hour

		authCache.setCacheSize(1000);
		authCache.setTimeout(120000); // 2 minutes
		authCache.setForceTimeout(120000); // 2 minutes
	}
	
	@Override
	protected Properties validateAuthRequest(User user, Properties input) throws PTException {
		Properties p = super.validateAuthRequest(user, input);
		
		// Validate that client_id / redirect_uri is valid
		// Could also check if state or other input parameters are valid
		
		return p;
	}

	@Override
	protected Properties handleTokenRequest(User user, Properties input) throws PTException {
		Properties p = validateTokenRequest(user, input);

		// Exchange authorization_code with a grant token
		
		if (OAuth2Helper.AUTHORIZATION_CODE.equals(input.getProperty(OAuth2Helper.GRANT_TYPE))) {
			String authorizationCode = input.getProperty(OAuth2Helper.CODE);
			
			AuthCacheEntry entry = (AuthCacheEntry) authCache.get(authorizationCode);
			
			if (entry == null || !entry.clientid.equals(input.getProperty(OAuth2Helper.CLIENT_ID )) || !input.getProperty(OAuth2Helper.CLIENT_SECRET).equals(entry.clientSecret)) {
				throw new PTException("Invalid authorization_code, client ID or client secret", AuthErrorCodes.ERROR_INVALIDCREDENTIALS, "Invalid authorization_code, client_id or client_secret");				
			}
			if (!entry.redirectUri.equals(input.getProperty(OAuth2Helper.REDIRECT_URI ))) {
				throw new PTException("Invalid redirect URI", AuthErrorCodes.ERROR_INVALIDCREDENTIALS, "Invalid redirect URI");				
			}

			p.setProperty(OAuth2Helper.ACCESS_TOKEN, entry.bearertoken);
			p.setProperty(OAuth2Helper.TOKEN_TYPE, "Bearer");
			p.setProperty(OAuth2Helper.EXPIRES_IN, "3600");
			
			// Here, we could also add a refresh_token if we had one to add.
		} else { // Must be refresh_token, since validateTokenRequest ensures nothing else can pass
			@SuppressWarnings("unused")
			String refreshToken = input.getProperty(OAuth2Helper.REFRESH_TOKEN);
		
			throw new PTException("Refresh tokens are not supported at the moment", AuthErrorCodes.ERROR_INVALIDCREDENTIALS, "refresh_token not supported");
		}
		
		return p;
	}
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	@Override
	protected java.util.Properties handleAuthRequest(dk.itp.security.passticket.User user, java.util.Properties input) throws PTException {
		Properties p = validateAuthRequest(user, input);
		
		String clientid = input.getProperty(OAuth2Helper.CLIENT_ID);

		// Create a new clone and use this to avoid altering the current users original session
		User newUser = (User) user.clone();
		if (newUser.stateVariables == null)
			newUser.stateVariables = new Hashtable();
						
		// Save the requested scope for later use when the session is restored
		newUser.stateVariables.put("pp_oauth2_scope", input.getProperty(OAuth2Helper.SCOPE));
		// Save the client ID too for later use
		newUser.stateVariables.put("pp_oauth2_clientid", input.getProperty(OAuth2Helper.CLIENT_ID));
		
		byte[] persistedSession;
		try {
			persistedSession = persistSession(newUser);
		} catch (IOException e) {
			throw new PTException(e);
		}

		if (input.getProperty(OAuth2Helper.RESPONSE_TYPE).equalsIgnoreCase(OAuth2Helper.CODE)) {
			// Tell dispatcher to disable IP checking for this particular session(bearer token), since it is meant
			// to be used by another server instead of the same client
			newUser.stateVariables.put("pp_disable_ipchecking", "true");			
		}

		String bearertoken = Base64.encode(UUID.randomUUID().toString());
		cache.put(bearertoken, persistedSession);
						
		
		if (input.getProperty(OAuth2Helper.RESPONSE_TYPE).equalsIgnoreCase(OAuth2Helper.TOKEN)) {
			// Implicit grant
			p.setProperty(OAuth2Helper.ACCESS_TOKEN, bearertoken);
			p.setProperty(OAuth2Helper.TOKEN_TYPE, "Bearer");
			p.setProperty(OAuth2Helper.EXPIRES_IN, "3600");
			return p;			
		} else {
			// Normal flow
			String authorizationToken = UUID.randomUUID().toString();
			
			AuthCacheEntry entry = new AuthCacheEntry();
			entry.bearertoken = bearertoken;
			entry.clientid = clientid;
			entry.redirectUri = input.getProperty(OAuth2Helper.REDIRECT_URI);
			authCache.put(authorizationToken, entry);
			
			p.setProperty(OAuth2Helper.AUTHORIZATION_CODE, authorizationToken);
			return p;			
		}
	}
	
	
	@Override
	public void createFromTicket(User user) throws PTException {
		// Ticket is in user.ticket
		
		byte[] ba = (byte[]) cache.get(user.ticket);
		
		if (ba == null)
			throw new PTException("Invalid OAuth ticket", AuthErrorCodes.ERROR_INVALIDCREDENTIALS, "Invalid or expired OAuth ticket");
		
		try {
			User restoredUser = restorePersistedSession(ba);
			
			// Save the ticket and session ID so we do not overwrite them
			restoredUser.ticket = user.ticket;
			restoredUser.sessionID = user.sessionID;
			
			// Restore everything from the persisted session to this one
			restoredUser.copyTo(user);
			
			user.addHistory("Session restored from persisted session using OAuth ticket: " + user.ticket);
		} catch (IOException e) {
			throw new PTException("Unable to restore persisted session", AuthErrorCodes.ERROR_GENERALERROR, "Unable to restore session", e);
		}
	}
}


... and here is an example of a plugin which uses LDAP to lookup user information.

package dk.portalprotect.sample.plugins.ldap;
import java.io.IOException;
import java.util.Properties;
import java.util.Vector;

import netscape.ldap.LDAPException;
import netscape.ldap.util.DN;
import org.slf4j.LoggerFactory;
import dk.itp.configuration.ConfigurationMacroHandler;
import dk.itp.managed.service.gui.ListNameValuePairs;
import dk.itp.security.authentication.DefaultAuthenticationPlugin;
import dk.itp.security.authentication.IAuthenticationPlugin;
import dk.itp.security.authorization.client.PasswordSSHA;
import dk.itp.security.exception.NotFoundException;
import dk.itp.security.exception.NotUniqueException;
import dk.itp.security.ldap.LdapAuthorizationPlugin;
import dk.itp.security.ldap.LdapGroupConnection;
import dk.itp.security.ldap.LdapUser;
import dk.itp.security.ldap.LdapUserConnection;
import dk.itp.security.passticket.PTException;
import dk.itp.security.passticket.User;
import dk.itp.security.utils.PasswordUtils;

/**
 * @author Bo Friis
 * 
 * <pre>
 * PortalProtect - Security infrastructure
 * Copyright(c) 2001, IT Practice A/S, All rights reserved.
 * 
 * This source code is confidential.
 * </pre>
 */
public class LoginHandlerLdapImpl extends DefaultAuthenticationPlugin {
	protected org.slf4j.Logger log = LoggerFactory.getLogger(this.getClass().getName());

	private LdapUserConnection ldapUsers;
	private LdapGroupConnection ldapGroups;
	
	private String servers = "localhost:389";
	private String baseDN = ""; // "dc=itpractice";
	private String systemuser = ""; // "uid=admin, ou=Administrators, ou=TopologyManagement, o=NetscapeRoot";
	private String systempassword = ""; // "password";
	private String usersRDNName = "ou=People"; // "ou=People";
	private String useridName = "uid"; //"uid";
    private String ldap_userAttributes = "cn,uid,sn,initials,userpassword,memberof,ibm-allGroups";
    
    private boolean bindUsers = false;

    private String impersonateFile = null; 
    
	/**
	 * Try to change the userid and password for this user
	 */
	public void changePassword(User user, String oldPassword, String newPassword) throws PTException {
		LdapUserConnection lc = null;
		try {
			if (bindUsers) {
				String uid;
				if (usersRDNName.length() > 0)
					uid = useridName + "="+user.userid+", " +  usersRDNName + ", " + baseDN;
				else
					uid = useridName + "="+user.userid+", " +  baseDN;
				
				lc = new LdapUserConnection(servers, new DN(baseDN), uid, user.password, usersRDNName, ldap_userAttributes);
				LdapUser lu =lc.getUser(useridName, user.userid);
				lc.setPassword(lu.dn, newPassword);
				log.debug("password changed for user: " + user.userid);
			} else {
				LdapUser lu = getLdapUserConnection().getUser(useridName, user.userid);
				if (user.password.equals(oldPassword)) {
					getLdapUserConnection().setPassword(lu.dn, newPassword);
					log.debug("password changed for user: " + user.userid);
				} else {
					throw new PTException("invalid credentials");
				}
			}
		} catch(PTException e) {
			throw e;
		} catch (Throwable t) {
			log.error("Exception changing users password in LDAP", t);
			throw new PTException("ldap login handler");
		}
		finally {
			if (lc != null)
				try {lc.disconnect(); } catch(Exception t) {}
		}
	}

	/**
	 * This method checks user confirmations - a confirmation can be a digital signature, if the authentication method
	 * supports it, but it can also be a simple re-authentication, e.g. in case of userid/password authentication.
	 *
	 * Digital signatures is ofcourse preferred, since they offer improved security
	 *
	 */
	public void confirm(dk.itp.security.passticket.User user, java.lang.String signtext, java.lang.Object confirmObject)
		throws dk.itp.security.passticket.PTException {
		throw new PTException("Confirmation not supported by this authentication plugin");
	}

	/**
	 * Returns the authentication level, the higher the level, the more secure this authentication is considered to be.
	 */
	public int getAuthenticationLevel() {
		return 2;
	}

	/**
	 * Returns the type of authentication, all plugins have assigned a particular id identifying that specify type
	 * of authentication.
	 */
	public int getAuthenticationType() {
		return dk.itp.security.passticket.AuthTypes.AUTHTYPE_LDAPUIDPW;
	}

	private synchronized LdapUserConnection getLdapUserConnection() {
		if (ldapUsers == null) {
			ldapUsers = new LdapUserConnection(servers, new DN(baseDN), systemuser, systempassword, usersRDNName, ldap_userAttributes);
		}
		return ldapUsers;
	}
	
	private synchronized LdapGroupConnection getLdapGroupConnection() {
		if (ldapGroups == null) {
			ldapGroups = new LdapGroupConnection(servers, new DN(baseDN), systemuser, systempassword, usersRDNName, ldap_userAttributes);
		}
		return ldapGroups;
	}

	/**
	 * Returns the name of this particular authentication method
	 */
	public java.lang.String getName() {
		return "LDAP userid/password authentication";
	}

	/**
	 * Try to login the specified user session, using the supplied userid and password.
	 */
	public void login(User user, String userid, Object credentials) throws PTException {
		login(user, userid, credentials, null);
	}


    public LdapUser loginUser(String userid, Object credentials) throws PTException, LDAPException, NotUniqueException, NotFoundException {
        LdapUserConnection lc = null;
        LdapUser lu;
        try {
            if(bindUsers) {
				String uid;
				if (usersRDNName.length() > 0)
					uid = useridName + "="+userid+", " +  usersRDNName + ", " + baseDN;
				else
					uid = useridName + "="+userid+", " +  baseDN;
                lc = new LdapUserConnection(servers, new DN(baseDN), uid, credentials.toString(), usersRDNName, ldap_userAttributes);
                lu = lc.getUser(useridName, userid);
            } else {
                lu = getLdapUserConnection().getUser(useridName, userid);
                if(lu.userPassword == null || !PasswordSSHA.checkPassword(lu.userPassword, credentials.toString()))
                    throw new PTException("invalid credentials");
            }
        } finally {
            if (lc != null)
                try {lc.disconnect(); } catch(Exception t) {}
        }
        
        return lu;
    }
    
    public Vector getGroupsForUser(String userDN, String groupMemberAttr) throws PTException, LDAPException, NotUniqueException, NotFoundException {
        LdapGroupConnection lc = null;
        try {
           	lc = getLdapGroupConnection();
           	return lc.getGroupsForUser(userDN, groupMemberAttr);
        } finally {
            if (lc != null)
                try {lc.disconnect(); } catch(Exception t) {}
        }
    }
    
    /**
     * Reads a user record without doing any password checking - note that this method only works if bindUsers is turned off.
     * 
     * @param userid
     * @throws LDAPException 
     * @throws NotFoundException 
     * @throws NotUniqueException 
     */
    public LdapUser getUserRecord(String userid) throws NotUniqueException, NotFoundException, LDAPException {
        LdapUserConnection lc = null;
        LdapUser lu;
        try {
        	String impersonatedUserid  = impersonate(userid); 
            lu = getLdapUserConnection().getUser(useridName, impersonatedUserid);
            
            return lu;
        } finally {
            if (lc != null)
                try {lc.disconnect(); } catch(Exception t) {}
        }
    }
    
	/**
	 * Try to login the specified user session, using the supplied userid and password.
	 */
	public void login(User user, String userid, Object credentials, Object newPassword) throws PTException {
		try {
			LdapUser lu = loginUser(userid, credentials);
			
			user.customerID = userid; // not currently stored in ldap
			user.password = credentials.toString();
			user.isInternalUser = false; // not currently stored in ldap
			user.isLoggedOn = true;
			user.password = credentials.toString();
			user.userid = lu.dn.toString();
			user.username = lu.cn;
			if (lu.memberOf.size() > 0) {
				user.userGroups = new String[lu.memberOf.size()];
				lu.memberOf.copyInto(user.userGroups);
			}
			
		} catch(PTException e) {
			throw e;
		} catch (LDAPException e) {
			log.warn("Exception trying to log user " + userid + " into LDAP server", e);
			throw new PTException("LDAP: " + e.toString());
		} catch (NotFoundException e) {
			throw new PTException("user " + userid + " does not exist");
		} catch (NotUniqueException e) {
			throw new PTException("not valid user, user " + userid + " not unique");
		}
	}

	/**
	 * Sets the configuration - the login handler can read from the properties to determine its own configuration.
	 */
	public synchronized void setConfiguration(java.util.Properties props) {
		boolean mustRestart = false;

		String newServers = props.getProperty("ldap.servers", servers);
		String newBaseDN = props.getProperty("ldap.basedn", baseDN);
		String newSystemuser = props.getProperty("ldap.systemuser", systemuser);
		String newSystempassword = PasswordUtils.getPassword(props.getProperty("ldap.systempassword", systempassword));
		String newusersRDNName = props.getProperty("ldap.usersRDN", usersRDNName);
		boolean newBindUsers = props.getProperty("ldap.bindUsers", "false").equalsIgnoreCase("true");
        String newldap_userAttributes = props.getProperty("ldap.userattributes", "cn,uid,sn,initials,userpassword,memberof,ibm-allGroups,mail,userPrincipalName");

		if (!servers.equals(newServers)
			|| !baseDN.equals(newBaseDN)
			|| !systemuser.equals(newSystemuser)
			|| !systempassword.equals(newSystempassword)
			|| !usersRDNName.equals(newusersRDNName)
			|| bindUsers != newBindUsers)
			mustRestart = true;

		servers = newServers;
		baseDN = newBaseDN;
		systemuser = newSystemuser;
		systempassword = newSystempassword;
		usersRDNName = newusersRDNName;
		bindUsers = newBindUsers;
        ldap_userAttributes = newldap_userAttributes;

		if (mustRestart) {
			ldapGroups = null;
			ldapUsers = null;
		}

		// Properties that does not require restart			
		useridName = props.getProperty("ldap.useridName", "uid");
		
		// Only attempt to login if bindusers is true
		if (bindUsers) {
			try {
				getLdapUserConnection().getUser(useridName, systemuser);
			} catch (LDAPException e) {
				log.error("Exception trying to connect to LDAP", e);
			} catch (NotUniqueException e) {
				log.error("Could not find system user " + systemuser);
			} catch (NotFoundException e) {
				log.error("Could not find system user " + systemuser);
			}
		}
		
		impersonateFile = props.getProperty("ldap.impersonate");

	}
	
	private String impersonate(String userid) {
		if (impersonateFile==null)
			return userid;
		
		java.io.FileInputStream fis = null;
		try {
			fis = new java.io.FileInputStream(ConfigurationMacroHandler.expandSystemMacro(impersonateFile));
			Properties mapping = new Properties();
			mapping.load(fis);
			if (mapping.getProperty(userid)==null) {
				log.debug("could not find user "+userid+" in impersonation mapping");
				return userid;
			} else {
				return mapping.getProperty(userid);
			}
		} catch(IOException e) {
			log.error("could not impersonate user",e);
			return userid;
		} finally {
			if (fis!=null)
				try {
					fis.close();
				} catch (IOException e) {
				}
		}
		
	}
	
	/**
	 * @see IAuthenticationPlugin#modifyState(User, String, String)
	 */
	public void modifyState(User user, String key, String value) {}

	public String getStatusText() {
		ListNameValuePairs lvp = new ListNameValuePairs("LDAP connections");
		lvp.setProperty("user connection", getLdapUserConnection().getStatusText());
		lvp.setProperty("group connection", getLdapGroupConnection().getStatusText());
		return lvp.toHtml();
	}
}

Authentication Plugins using Scripts

Starting with Ceptor v5.70.2, you can write authentication plugins using scripts.

A script needs to create and return an instance of the IAuthenticationPlugin interface - this is easiest done by extending the class dk.itp.security.authentication.AbstractScriptAuthenticationPlugin which provide empty implementations for all methods, you can then override the methods you need.

Here is an example in javascript:

server.authenticationplugin.sample
var AuthTypes = Java.type('dk.itp.security.passticket.AuthTypes');
var PTException = Java.type('dk.itp.security.passticket.PTException');
var AuthErrorCodes = Java.type('dk.itp.security.passticket.server.AuthErrorCodes');

var MyPlugin = Java.extend(Java.type('dk.itp.security.authentication.AbstractScriptAuthenticationPlugin'));

var plugin = new MyPlugin() {
    getName: function() {
        return 'Dummy script based authentication plugin';
    },
   
    getAuthenticationType: function() {
        return AuthTypes.AUTHTYPE_DUMMY;
    },
    
    getAuthenticationLevel: function() {
    	return 1;
    },
    
    login: function(session, userid, credentials) {
        if (userid === 'test' && credentials == 'password') {
            session.userid='Dummy';
            session.groups = null;
            session.stateVariables = null;
            session.isLoggedOn=true;
        } else {
            throw new PTException("Only test user is allowed", AuthErrorCodes.ERROR_INVALIDCREDENTIALS, "Invalid credentials");
        }
    }
}
plugin;

The script creates and returns a new instance of a class which extends from AbstractScriptAuthenticationPlugin - note that the script itself is only run once, after that the instance returned is added to the list of active authentication plugins, and whenever one of the methods are called, e.g. login() then the corresponding function is executed.

Note that when editing these scripts in the Ceptor Console the editor has trouble with the syntax for inheriting from java classes in javascript, so it will indicate errors on the left even if the script is valid.


Extending functionality of existing authentication plugins using scripts

Starting with version 6.2.7 of Ceptor, it is also possible to extend the functionality of other authentication plugins using scripts.

Two new properties are added; script.prelogon and script.postlogon which can contain a script that gets called before and after login has been done using another authentication plugin.

When called, the variable context is an instance of the LogonScriptContext class, containing this information:

public class LogonScriptContext {
	/** Session Controller */
	public PTSServer sessionController;
	/** The users session */
	public User session;
	/** The remote client making the login call */
	public Peer peer;
	/** Userid, or null in postlogin script */
	public String userid;
	/** Credentials, or null in postlogin script */
	public Object credentials;
	/** New password, if available, or null in postlogin script */
	public Object newPassword;
	/** Authentication plugin type - see AuthTypes class for details */
	public int authType;
}



  • No labels