Ceptor has support for a RADIUS Server and client that supports authentication and accounting requests.

Features

Launcher Configuration

In order to get the RADIUS Server started the radius service should be configured in the ceptor_launch.xml. The radius server does not require its own JVM to run, so if the existing capacity can handle it, it could as an example be a service defined in the session controller classloader/JVM – for example like this:

<classloader name="sessionctrl">
    <service name="radiusserver1"
             launcherclass="dk.itp.pp.radius.RadiusServerLauncher" />
</classloader>


The RADIUS Server will require a minimum of configuration defining listening ports and addresses as well as shared secrets and an authentication plugin to use for validating authorization requests. For detailed configuration see the configuration reference:

<server name="radiusserver1" type="radius" description="Radius server" extends="applications">
  <group name="listening" description="Radius server listening configuration">
    <property name="authentication.listenport" value="1812" description="Authentication port"/>
    <property name="authentication.listenaddress" value="192.168.52.1" description="Authentication listening address - empty is default adapter"/>
    <property name="accounting.listenport" value="1813" description="Accounting port"/>
    <property name="accounting.listenaddress" value="127.0.0.1" description="Accounting listening address - empty is default adapter"/>
  </group>	
  <group name="authentication" description="Radius server security configuration">
    <property name="authtype.pap" value="9" description="Login authtype to verify user passwords"/>
  </group>	
  <group name="security" description="Radius server security configuration">
     <property name="sharedsecret.1" value="*=secret1" description="shared secret for all other clients than listed below"/>	
     <property name="sharedsecret.2" value="127.0.0.1,10.0.0.1=secret2" description="shared secret for IP 127.0.0.1 and 10.0.0.1"/>	
  </group>	
</server>


There are several more configuration parameters that can be set. 


Radius accounting is handled through logging account requests. The requests are logged through an appender "RadiusAccounting". Add configuration for this appender to log in a specific file if requred. If not the accounting entries will end up in the standard log file for the Radius server. The configuration for this is part of the default distribution and can be found in logback.xml in CEPTOR_HOME/pp/classes

Two factor (challenge) handling

The Radius server support two factor logins. It is supported with authentication plugins that can either validate the userid/password on the first authentication request and then prompt for a challenge after login and also with authentication plugins using the newToken method on first request and not validating the userid/password untill the challenge is validated.

In the first scenario the login method must throw a PTException with the error code "AuthErrorCodes.ERROR_NEED_OTP" for the radius server to send a challenge back to the radius client.


In both scenarios the authentication plugin called (typically the PAP authentication plugin) can set a session state variable "radius.challenge" which will be used as the challenge text on clients that supports this. This could be user friendly is for example showing the last 2 or 3 digits of the telephone number to help the user. If this session state variable is not set, the configuration parameter "authentication.challenge" is used instead if defined.  See configuration reference for more information. 


Setting up properties for two factor login using the user administration database and SMS/OTP challenges could look like this (other properties are not shown) if using the dk.itp.portalprotect.useradmin.server.SMSUAAuthenticationPlugin (configuration described here):


<server name="radiusserver1" type="radius" description="Radius server" extends="applications">
 ....
  <group name="authentication" description="Radius server security configuration">
    <property name="authtype.pap" value="43" description="Login authtype to verify user passwords in user admin database"/>
    <property name="authtype.challenge" value="43" description="Login authtype used to verify the challenge"/>
    <property name="authentication.challenge" value="Please enter the code received as SMS: " description="The challenge text to show to the user"/>
  </group>	
 ....
</server>

Scripting

Ceptor Radius module supports two different types of scripts;

For accounting, specify the script in the accounting.script configuration option.

The Authentication script allows to override the exact behavior when receiving an access request packet from a Radius client, and the Accounting script allows handling an incoming accounting packet, parsing the contents and saving the values where you need to, in case the default behavior of writing it to a SLF4J logging appender is insufficient.

If you specify the configuration authentication.script then you can specify a script (Javascript / Python or Groovy) which will be executed - this allows you to customize handling of the incoming request.

If a script is specified, it overrides the other options for authtype.pap, authtype.challenge etc.

When the script is called, it has a variable called context which contains the following information:

public class RadiusContext {
	/** Session ID - also used as state */
	public String session;
	/** Ceptor Agent */
	public RadiusService agent;
	/** Incoming access request */
	public AccessRequest accessRequest;
	/** Reply that will be sent */
	public RadiusPacket reply;
	/** Incoming accounting request, if this script is called when an accounting packet arrives */
	public AccountingRequest accountingRequest;
	/** Client making the call */
	public InetSocketAddress client;
	/** Authenticator where you can obtain shared secret for a particular client if needed */
	public RadiusServerAuthenticator authenticator;
}


import dk.itp.security.passticket.*;
import dk.itp.security.passticket.server.AuthErrorCodes;
import dk.itp.pp.radius.packet.*;
import dk.itp.pp.radius.attribute.*;

// Change these to match required data
int authenticationType = AuthTypes.AUTHTYPE_GOOGLEAUTH;
String otpPrompt = "Enter OTP displayed in Authenticator: ";

if (context.session) {
    // We have a session, so it must be 2nd call with an OTP
	String userPassword = context.accessRequest.getUserPassword();
	
	def challenge = new Object[1];
	challenge[0] = userPassword;
	
    context.agent.logon(context.session, authenticationType, context.agent.getUser(context.session), challenge);
    context.reply.addAttribute( new StringAttribute( TypeConstants.STATE, context.session) );
    context.reply.setPacketType(TypeConstants.PACKAGE_TYPE_ACCESS_ACCEPT);
} else {
    // Starting from scratch, create a new session and authenticate into it
    
    def sourceIP = context.accessRequest.getAttributeValue("NAS-IP-Address");
    if (!sourceIP)
        sourceIP = context.accessRequest.getAttributeValue("Calling-Station-Id");
    context.session = context.agent.newSession(sourceIP);
    
	String userName = context.accessRequest.getUserName();
	String userPassword = context.accessRequest.getUserPassword();
    
    try {
        context.agent.logon(context.session, authenticationType, userName, userPassword);
    } catch(PTException e) {
        if( e.getErrorCode() == AuthErrorCodes.ERROR_NEED_OTP ) {
            context.reply.addAttribute( new StringAttribute( TypeConstants.STATE, context.session) );
			context.reply.addAttribute( "Reply-Message", otpPrompt );
            context.reply.setPacketType(TypeConstants.ACCESS_CHALLENGE);
        } else {
            throw e;
        }
    }
}


Full configuration example

	<server name="radiusserver1" type="radius" description="Radius server" extends="applications">
		<group name="listening" description="Radius server listening configuration">
			<property name="accounting.listenaddress" value="127.0.0.1" description="Accounting listening address - empty is default adapter"/>
			<property name="accounting.listenport" value="1813" description="Accounting port"/>
			<property name="authentication.listenaddress" value="0.0.0.0" description="Authentication listening address - empty is default adapter"/>
			<property name="authentication.listenport" value="1812" description="Authentication port"/>
			<property name="sockettimeout" value="3000" description="Listening timeout for authentiation and accounting"/>
		</group>
		<group name="other" description="Radius server configuration">
			<property name="authentication.challenge" value="43" description="Challenge authentication plugin if PAP authentication plugin supports two factor logins"/>
			<property name="authentication.pap" value="9" description="PAP authentication plugin (for example UAAuthenticationPlugin)"/>
			<property name="authentication.script" value="%{script:groovy}import dk.itp.security.passticket.*;
import dk.itp.security.passticket.server.AuthErrorCodes;
import dk.itp.pp.radius.packet.*;
import dk.itp.pp.radius.attribute.*;

// Change these to match required data
int authenticationType = AuthTypes.AUTHTYPE_GOOGLEAUTH;
String otpPrompt = &quot;Enter OTP displayed in Authenticator: &quot;;

if (context.session) {
    // We have a session, so it must be 2nd call with an OTP
	String userPassword = context.accessRequest.getUserPassword();
	
	def challenge = new Object[1];
	challenge[0] = userPassword;
	
    context.agent.logon(context.session, authenticationType, context.agent.getUser(context.session), challenge);
    context.reply.addAttribute( new StringAttribute( TypeConstants.STATE, context.session) );
    context.reply.setPacketType(TypeConstants.PACKAGE_TYPE_ACCESS_ACCEPT);
} else {
    // Starting from scratch, create a new session and authenticate into it
    
    def sourceIP = context.accessRequest.getAttributeValue(&quot;NAS-IP-Address&quot;);
    if (!sourceIP)
        sourceIP = context.accessRequest.getAttributeValue(&quot;Calling-Station-Id&quot;);
    context.session = context.agent.newSession(sourceIP);
    
	String userName = context.accessRequest.getUserName();
	String userPassword = context.accessRequest.getUserPassword();
    
    try {
        context.agent.logon(context.session, authenticationType, userName, userPassword);
    } catch(PTException e) {
        if( e.getErrorCode() == AuthErrorCodes.ERROR_NEED_OTP ) {
            context.reply.addAttribute( new StringAttribute( TypeConstants.STATE, context.session) );
			context.reply.addAttribute( &quot;Reply-Message&quot;, otpPrompt );
            context.reply.setPacketType(TypeConstants.ACCESS_CHALLENGE);
        } else {
            throw e;
        }
    }
}
" description="Script executed to perform authentication - when set, authentication.pap, authentication.twofactor and authentication.chap are ignored"/>
			<property name="authentication.twofactor" value="false" description="Allows the radius server to do a twofactor login. For two factor logins using the newToken method (google authenticator for example)"/>
			<property name="clientsessions.forcetimeout" value="30" description="Force timeout in minutes for client sessions"/>
			<property name="clientsessions.maxcount" value="100000" description="Number of client sessions to store in the server"/>
			<property name="clientsessions.timetolive" value="5" description="Time to live for client sessions if not seen"/>
			<property name="packet.debug" value="true" description="Set to true to log contents of radius packets (as INFO - will be logged as DEBUG by otherwise)"/>
			<property name="ppsessions.forcetimeout" value="30" description="Force timeout in minutes for pp sessions"/>
			<property name="ppsessions.maxcount" value="100000" description="Number of pp sessions to store in the server"/>
			<property name="ppsessions.timetolive" value="5" description="Time to live for pp sessions if not seen"/>
			<property name="username.sessionid" value="true" description="Set the User-Name attribute on accept packages to PP session ID. Some clients might send it back. Some might not support it!"/>
		</group>
		<group name="security" description="Radius server security configuration">
			<property name="sharedsecret.1" value="*=secret1" description="shared secret for all other clients than listed below"/>
			<property name="sharedsecret.2" value="127.0.0.1,10.0.0.1=secret2" description="shared secret for IP 127.0.0.1 and 10.0.0.1"/>
		</group>
	</server>