Configuring the RADIUS facade
A simple case
Every RADIUS service requires a domain where the transformed requests are sent to. This simple example authenticates RADIUS requests against the test-AuthEngine.
<Domain name="RADIUS_TEST" default="false">
<Entry method="authenticate" state="RadiusTest"/>
<Entry method="stepup" state="RadiusTest"/>
</Domain>
Requests received on the RADIUS authPort trigger "authenticate", on the acctPort trigger a "stepup".
<AuthState name="RadiusTest" class="ch.nevis.esauth.auth.engine.UidPwLoginTest" authLevel="auth.test" final="false">
<ResultCond name="ok" next="AuthDone"/>
<ResultCond name="firstlogin" next="AuthDone"/>
<Response value="AUTH_ERROR">
<Gui name="AuthErrorDialog"/>
</Response>
</AuthState>
This example uses the UidPwLoginTest AuthState. It accepts authentication whenever username and password are identical.
The UidPwLoginTest AuthState is meant for integration testing purposes only. Never use this AuthState in productive setups!
<RadiusService SSODomain="RADIUS_TEST" listenAddress="localhost" secret="secret" stateless="true">
<RadiusInput type="User-Name" inArg="isiwebuserid"/>
<RadiusInput type="User-Password" inArg="isiwebpasswd"/>
<RadiusResponse code="Access-Accept" if="${response:Result:(ok|firstlogin)}">
<RadiusAttribute type="Reply-Message"
value="Access granted to ${inargs.isiwebuserid}!"/>
</RadiusResponse>
<RadiusResponse code="Access-Reject">
<RadiusAttribute type="Reply-Message"
value="Access Rejected per default..."/>
</RadiusResponse>
</RadiusService>
This starts a RADIUS service with default properties listening on localhost. The shared secret between the server and all its clients is "secret". The input mapping (RADIUS to Nevis) consists of two mappings. The first will map the RADIUS attribute User-Name to the default inArg for the username. The request is sent to the domain RADIUS_TEST.
When the AuthEngine finished processing, a RadiusResponse is selected. In this case, the first possible response is an "Access-Accept" packet. It is built if the last transition signaled that it was "ok" or "firstlogin" and carries a single AVP "Reply-Message".
If the response of the AuthEngine does not match the first response rule, an "Access-Reject" packet is returned. The default response does not contain variable substitution (since input may not be provided).
mTAN
In this example, the TANState is used in combination with RADIUS. A RADIUS packet is sent, triggering an OTP to be sent to the user's e-mail address and returning an access challenge to the user.
<Domain name="RADIUS_TEST_TAN" default="false">
<Entry method="authenticate" state="RadiusTestTAN_pwd"/>
</Domain>
...
<AuthState name="RadiusTestTAN_pwd"
class="ch.nevis.esauth.auth.engine.UidPwLoginTest"
final="false" authLevel="auth.test">
<ResultCond name="ok" next="RadiusTestTAN"/>
<ResultCond name="firstlogin" next="RadiusTestTAN"/>
<ResultCond name="default" next="AuthError"/>
<Response value="AUTH_CONTINUE">
<Gui name="AuthPasswordDialog"/>
</Response>
</AuthState>
<AuthState name="RadiusTestTAN"
class="ch.nevis.esauth.auth.states.tan.TANState"
final="false" authLevel="auth.test">
<ResultCond name="ok" next="AuthDone"/>
<Response value="AUTH_CONTINUE">
<Gui name="AuthTANDialog"/>
</Response>
<property name="response" value="${inargs:isiwebpasswd}"/>
<property name="sender" value="noreply"/>
<property name="recipient" value="${inargs:isiwebuserid}@example.com"/>
<property name="smtpHost" value="smtp-dev.example.com"/>
<property name="messageTemplate" value="${sess:mtan.challenge}"/>
<property name="tanTemplate" value="6{ABCDEFGHJKLMNPQRSTUVWXYZ}"/>
</AuthState>
...
<RadiusService SSODomain="RADIUS_TEST_TAN" listenAddress="localhost" secret="secret">
<RadiusInput type="User-Name" inArg="isiwebuserid"/>
<RadiusInput type="User-Password" inArg="isiwebpasswd"/>
<!-- map State to sessionId, establishing session-association -->
<RadiusInput type="State" inArg="sessionid"/>
<!-- Access-Accept, if displaying AuthDoneDialog-->
<RadiusResponse code="Access-Accept" if="${response:guiName:AuthDoneDialog}">
<RadiusAttribute type="Reply-Message"
value="Access granted to ${inargs.isiwebuserid}!"/>
</RadiusResponse>
<!-- Access-Reject, if status is AUTH_ERROR (=2)-->
<RadiusResponse code="Access-Reject" if="${response:status:2}">
<RadiusAttribute type="Reply-Message"
value="Error Access Rejected: ${notes.lasterrorinfo}"/>
</RadiusResponse>
<!-- Access-Challenge, if status is AUTH_CONTINUE (=0)-->
<RadiusResponse code="Access-Challenge" if="${response:status:0}">
<RadiusAttribute type="Prompt" value="No-Echo"/>
<RadiusAttribute type="Reply-Message"
value="new tokencode required: ${notes:lasterrorinfo}"/>
<!-- send sessionId to client, enabling session-association -->
<RadiusAttribute type="State" value="${sess:Id}"/>
</RadiusResponse>
</RadiusService>
In this scenario, the client will send an access request with the RADIUS attributes User-Name and User-Password set. These attributes are mapped to the inArgs and validated by the UidPwLoginTest AuthState. Upon success an OTP is sent to the client over a second channel and an access challenge is returned. This packet contains also an attribute State, which the client has to return unmodified with the next packet.
In a second step, the client will send another access request. It must contain User-Name, User-Password and State. This time, the User-Password is expected to be the OTP. Because of the mapping of the State attribute to the session ID, the request will be passed to the TANState AuthState. Upon success an access accept is returned.
From a security perspective, it is a good idea to send an access challenge regardless of the user ID/password. Upon failure, inform the user that the password or the OTP were wrong.