The description of EAP is beyond the scope of these instructions. Refer to rfc3748.txt (EAP) and rfc2869.txt (RADIUS Extension).
The server's basic EAP classes are EAPInfo, EAPState, and EAPPacket. EAPInfo allows access to EAP information through the AccessImpl class - your code implementing the authentication methods. EAPPacket decodes / encodes and handles a number of EAP packet types including Identity, Success, Failure, and so on. The EAPState class assists in tracking EAP information from one EAP transaction to another.
EAPInfo is access through the AuthInfo object. If the AuthInfo method getEAP()
returns null there is no EAP-Message attribute availailable. Otherwise
it will return an EAPInfo object that allows you to manipulate EAP packets.
handleStartPacket()
In the rare event that an actual EAP-Start of the plainest forms arrives (an EAP packet with only a request code set) this method requests the usual first packet - the identity response. The method returns true if an EAP-Start packet arrives in which case the AccessImpl.authenticate() method should return. If no EAP-Start packet arrived it returns false. EAP-Start is rarely used with RADIUS since it cannot easily be proxied (there is no User-Name attribute), although other attributes may imply relaying.
getPacket()
Gets the EAPPacket class from the EAPInfo class. The EAPPacket methods are described below.
createChallenge16()
Creates a random 16 byte challenge.
getCode, getType(), getData()
Get the EAP code, type, and data fields respectively.
setCode, setType(), setData(int type, byte[] data), setData(byte[] data)
Set the code, type and data respectively. The first form of setData() is more commonly used which avoids the call to setType().
toUTF8()
Converts a string to UTF8 bytes.
createPacketIdentifier(), getPacketIdentifier()
Create a new random packet Identifier, or retrieve the one in the current packet.
isNAK(), isRequest(), isResponse(), isSuccess(), isIdentity(), isFailure(), isEAPStart()
Determine qualities of a packet.
toAttributeList()
Convert an EAP packet into an attribute list suitable for sending in a RADIUS packet. Multiple EAP-Message attributes may be created. The Message-Authenticator attribute is also created.
createFailureResponse(), createSuccessResponse(), createNotificationResponse(), createNotificationRequest(), createNAKResponse(), createMD5Response(), createMD5Request(), createIdentityResponse()
Creates several types of responses and requests. These produce canned attribute lists of particular requests and responses instead of using setCode(), setData(), setPacketIdentifier() and so on.
The EAPMD5 authentication looks like this:
The example code in the examples/server/FileAccess.java
import com.theorem.radserver3.eap.*;
public void authenticate(AuthInfo auth){
...
EAPInfo eapi= auth.getEAP();
if (eapi.handleStartPacket(null))
return;
EAPPacket ep = eapi.getPacket();
EAPMD5Auth md5Auth = new EAPMD5Auth(auth, ep);
if (md5Auth.MD5(passwordBytes) == false)
{
// Check the NAK value to see if a different authentication method was
// requested.
int nak = md5Auth.getNAK();
msg = "Unknown EAP authentication type requested: " + ep.getTypeName();
AccessRejectException areeap = new AccessRejectException(msg);
areeap.setAttributes(ep.createFailureResponse(ep.getPacketIdentifier()));
throw areeap;
}
return;
The actual code for the MD5 method follows. The method produces the necessary AccessDropExceptions in the case of malformed EAP packets, AccessRejectExceptions when authentication fails and Success packets when authentication succeeds.
Further the MD5 handler is able to preserve state information like the MD5 challenge value and the EAP packet identifier using the EAPMD5State class and the setStateObject() / getStateObject() / removeStateObject() methods that allow data persistence across transactions. First we expect an Identity packet. If it is an Identity packet the MD5 challenge is sent with the next expected state set to an MD5 response.. The MD5 response is checked for validity and the authentication is completed.
By setting the expected state we create a state machine that can easily determine if an incoming packet is the expected type and trap invalid sequences in the transaction.
Here's the MD5 EAP authentication:
package com.theorem.radserver3.eap;
import java.util.*;
import java.io.*;
import java.net.*;
import com.theorem.radserver3.*;
import com.theorem.radserver3.bouncycastle.*;
public class EAPMD5Auth
{
/**
* MD5 response is expected ({@value}).
*/
public static final int MD5 = 1;
/**
* Return value if the authentication completed succesfully.
*/
public static final int NO_NAK = 0;
/**
* Return value if no prefered authentication accomanpanied a NAK.
*/
public static final int NAK_AUTH_UKNOWN = -1;
private AuthInfo ai; // AuthInfo object for accessing server and global information.
private EAPPacket eapIn; // EAP Packet that arrived.
private int NAKAuthRequest; // Authentication requested by NAK.
/**
* Constructor.
*
* @param ai AuthInfo object.
* @param eapIn EAPPacket object.
*/
public EAPMD5Auth(AuthInfo ai, EAPPacket eapIn)
{
this.ai = ai;
this.eapIn = eapIn;
NAKAuthRequest = NO_NAK; // Assume no NAK will be received.
}
/**
* EAP MD5 handler.
* <p>
* This is similar to the handleStartPacket() in that it handles the entire transaction from
* the Identity Response to the actual authentication.
* <p>
* If successful it will have appended the EAP success message to the response attributes.
* <p>
* The sequence of packets:
* <br>Client --> Server: Identity Response
* <br>Server --> Client: MD5 Challenge
* <br>Client --> Server: MD5 Response
* <br>Server --> Client: Success or Failure Response.
*
* @param password Password.
*
* @return True if the authentication is preceeding normally.
* If False is returned check to see if a NAK was received.
AUTH_COMPLETE if the authentication completed.
* Returns the prefered authentication Type if a NAK was received.
* If no prefered authentication type accompanied the NAK NAK_AUTH_UKNOWN is returned.
*
* @throws AccessDropException if there's a problem with the EAP packet.
* @throws AccessRejectException if there's an authentication error.
*
* @see #getNAK() getNAK()
*/
public boolean MD5(byte password[]) throws AccessDropException, AccessRejectException
{
if (eapIn.getCode() != EAPPacket.CODE_RESPONSE)
throw new AccessDropException("EAP Packet code is not Response.");
AttributeList inList = ai.getRequestAttributeList();
byte name[] = inList.getBinaryAttribute(Attribute.User_Name);
if (name == null)
throw new AccessDropException("EAP Packet reply to " + eapIn.getTypeName() + " response packet missing User-Name attribute");
// If it's the Identity Response packet conveyed by a Access-Request don't get a State.
// A State would only exist if an EAP-Start packet was originally sent - a rare occurrence.
if (eapIn.getType() == EAPPacket.TYPE_IDENTITY)
{
if (RADIUSEncrypt.cmp(eapIn.getData(), name) == false)
throw new AccessDropException("EAP Packet reply to EAP-Identity response packet Identity doesn't match User-Name attribute");
EAPMD5State esmd5 = new EAPMD5State();
// Generate a new identifier.
esmd5.identifier = EAPPacket.createPacketIdentifier();
esmd5.state = MD5;
esmd5.name = name;
esmd5.challenge = EAPPacket.createChallenge16();
EAPPacket eapOut = new EAPPacket();
AttributeList outList = eapOut.createMD5Request(esmd5.identifier, esmd5.challenge);
outList.addAttribute(ai.setStateObject(esmd5));
ai.setResponseAttributes(outList);
ai.setResponseType(PacketType.Access_Challenge);
return true;
}
// Get our state object.
// If there's no session object drop the packet - it's timed out.
EAPState es = (EAPState) ai.getStateObject();
if (es == null)
throw new AccessDropException("EAP Packet reply to EAP-Identity response packet has no State attribute or has timed out.");
// Check that packet identifiers match.
if (es.identifier != eapIn.getPacketIdentifier())
throw new AccessDropException("EAP Packet reply to EAP-MD5 response packet identifier doesn't match request identifier");
// See if the MD5 authentication type is rejected.
if (eapIn.getType() == EAPPacket.TYPE_NAK)
{
byte data[] = eapIn.getData();
if (data.length == 0)
NAKAuthRequest = NAK_AUTH_UKNOWN;
else
NAKAuthRequest = (int) data[0] & 0xff;
return false;
}
if (eapIn.getType() == EAPPacket.TYPE_MD5)
{
// Get the md5 object.
EAPMD5State esmd5 = (EAPMD5State) ai.getStateObject();
ai.removeStateObject();
if (RADIUSEncrypt.cmp(inList.getBinaryAttribute(Attribute.User_Name), name) == false)
throw new AccessDropException("EAP Packet reply to EAP-MD5 response packet Identity doesn't match User-Name attribute");
int pwdLen = password.length;
int chLen = esmd5.challenge.length;
byte d[] = new byte[1 + pwdLen + chLen];
d[0] = (byte) esmd5.identifier;
System.arraycopy(password, 0, d, 1, pwdLen);
System.arraycopy(esmd5.challenge, 0, d, 1 + pwdLen, chLen);
MD5Digest md5 = new MD5Digest();
md5.update(d);
byte digest[] = md5.digest();
EAPPacket eapOut = new EAPPacket();
if (ai.cmp(digest, 0, eapIn.getData(), 0, digest.length))
{
AccessRejectException are = new AccessRejectException("EAP-Packet: MD5 Response failed to match.");
are.setAttributes(eapOut.createFailureResponse(esmd5.identifier));
throw are;
}
ai.appendResponseAttributes(eapOut.createSuccessResponse(esmd5.identifier));
ai.setResponseType(PacketType.Access_Accept);
} else
ai.removeStateObject();
return true;
}
/**
* Get any NAK information that might have been received.
*
* There are three possibilities:
* <ol>
* <li> No NAK was received - the value will be NO_NAK.</li>
* <li> A NAK was received suggesting another authentication method the value will be the requested EAP authentication method</li>
* <li> A NAK was received but suggested no better authentication method - the value will be NAK_AUTH_UKNOWN.</li>
* </ol>
* @return The value accompanying the NAK or NO_NAK, NAK_AUTH_UKNOWN.
* A legal authentication type will be in the range of 4 to 255.
*/
public int getNAK()
{
return NAKAuthRequest;
}
}