(update : check out demo 4 in the Flash demo for our ERP Firewall to see some additional real world examples of multi-factor authentication for PeopleSoft)
At the end of the previous blog entry on PeopleSoft telephony integration, we were able to initiate telephone calls from within PeopleCode to an end user and prompt them to enter a PIN code so that we could authenticate them. We could even see in the logs whether the user typed the correct PIN on their phone or not.
What we didn’t cover was how we can figure out in PeopleCode whether the user typed the correct PIN or not. If we can’t do that, then there’s not much point to the whole exercise, eh? 🙂
To make a long story less long, you can’t (easily) find out the answer with just PeopleCode. There are a couple of reasons for this. One was hinted at yesterday; the Asterisk Manager API call origination only allows you to get the call going. Once the call has been successfully initiated, then the call origination reports success. Even if the user does not answer the phone, the call itself was successfully originated.
If you want to find out what happened, then you have to use another part of the Asterisk Manager API, the events API. That allows you to listen to various events happening within Asterisk. So after we originate the call, there will be a series of events triggered (Dial events, Hangup events, Callerid events, etc.) that we can use.
The good news is that the Asterisk-Java library that we’re using has great support for the events API. There are Java classes for all of the different events that occur within the Asterisk server. For example, here’s someone else’s sample Java code of catching the Dial event and using that for screenpops.
In order to register which events that you are interested in though, the Asterisk-Java library require you to implement a Java interface called ManagerEventListener. Implementing interfaces is no big deal when you’re writing Java code; it’s just a list of methods that you have to provide the actual code for. However PeopleCode can’t implement Java interfaces, so we’re going to need to write some Java code ourselves.
In order to make this work, I needed to create two separate Java classes. The first one is called PSoftLoginManager. In addition to implementing the ManagerEventListener interface, I moved the PeopleCode logic for doing the call origination into this class as well.
The other class that I needed to create is called PSoftLoginEvent and it extends the delivered UserEvent class. The Authenticate command that we are using in Asterisk does not actually create any events for us to listen to. However, Asterisk supports the notion of user defined events, so we can use that. At the moment the PSoftLoginEvent includes the PeopleSoft user ID and whether the login attempt was successful or not.
The Java code wasn’t too bad. 100+ lines or so, but let’s look at how the PeopleCode looks now first. All of the variable declarations at the top stay the same, but the rest of the code is now just
Local JavaObject &loginMgr = CreateJavaObject("com.greysparling.asterisk.PSoftLoginManager", &host, &user, &pswd);
Local JavaObject &loginEvt = &loginMgr.challengeUser(&userID, &phone, &pinCode, &channel, &context, &exten);
If &loginEvt.isSuccess() Then
Warning ("Successfully validated user " | &userID);
Else
Warning ("Off with " | &userID | "'s head!");
End-If;
We create our PSoftLoginManager object with the Asterisk server information, and then call challengeUser to initiate the call and get the result back. Instead of having challengeUser return a binary result of success or not, I return the actual PSoftLoginEvent object itself which can be queried for success or failure. That keeps things easy later if we need to expose any additional data back from the Asterisk side.
On the Asterisk configuration side, we need a small change in order to trigger our login event.
[challenge-psft-user]
exten = 7189,1,Answer()
exten = 7189,2,Playback(vm-intro)
exten = 7189,3,NoOp(Authenticating user ${PSFTUSERID})
exten = 7189,4,Authenticate(${PIN},j)
exten = 7189,5,NoOp(Successful login ${PSFTUSERID})
exten = 7189,6,UserEvent(PSoftLogin|userId: ${PSFTUSERID}|result: SUCCESS)
exten = 7189,7,Hangup()
exten = 7189,105,NoOp(Unsuccessful login ${PSFTUSERID})
exten = 7189,106,UserEvent(PSoftLogin|userId: ${PSFTUSERID}|result: FAILURE)
exten = 7189,107,Hangup()
The main difference is the use of the Asterisk UserEvent command. That takes the name of our login event as it’s first parameter. The additional parameters are the ones that we have defined for our event. The Asterisk-Java library will automatically map these extra parameters that we have defined into the appropriate setter calls on the Java PSoftLoginEvent object (e.g. userId maps to setUserId() ).
Now when we run the App Engine program, here’s what the Asterisk console looks like.
-- Executing [7189@challenge-psft-user:1] Answer("IAX2/123456789-2", "") in new stack
-- Executing [7189@challenge-psft-user:2] Playback("IAX2/123456789-2", "gs-demo-login") in new stack
-- Playing 'gs-demo-login' (language 'en')
-- Executing [7189@challenge-psft-user:3] NoOp("IAX2/123456789-2", "Authenticating user PTDMO") in new stack
-- Executing [7189@challenge-psft-user:4] Authenticate("IAX2/123456789-2", "4554|j") in new sta ck
-- Playing 'agent-pass' (language 'en')
-- Playing 'auth-thankyou' (language 'en')
-- Executing [7189@challenge-psft-user:5] NoOp("IAX2/123456789-2", "Successful login PTDMO") in new stack
-- Executing [7189@challenge-psft-user:6] UserEvent("IAX2/123456789-2", "PSoftLogin|userId: PTDMO|result: SUCCESS") in new stack
-- Executing [7189@challenge-psft-user:7] Hangup("IAX2/123456789-2", "") in new stack
== Spawn extension (challenge-psft-user, 7189, 7) exited non-zero on 'IAX2/123456789-2'
-- Hungup 'IAX2/123456789-2'
You can see the addition of the UserEvent here, plugged in with the name of our event, the PTDMO user and the result value of SUCCESS. Since this event is what our challengeUser function returns to PeopleCode, we can now properly do the two factor authentication from PeopleSoft.
Here’s the Java code for the PSoftLoginEvent class. The things that are worth noting is that it extends the org.asteriskjava.manager.event.UserEvent class, and that it has setters for the values that we want to receive. It appears as though you can only get strings from the Asterisk side, so I adopted the convention that result has to be the string SUCCESS for a successful login.
package com.greysparling.asterisk;
import org.asteriskjava.manager.event.UserEvent;
public class PSoftLoginEvent extends UserEvent {
private String userId;
private String result;
public PSoftLoginEvent(Object source) {
super(source);
}
public String toString() {
return "PSoftLogin for " + getUserId() + " (" + result + "),"
+ super.toString();
}
public boolean isSuccess() {
return "SUCCESS".equals(result);
}
public void setResult(String result) {
this.result = result;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
private static final long serialVersionUID = 1L;
}
Here is the Java code for the PSoftLoginManager class. This is a little bit more complex, but not too bad. We implement the ManagerEventListener class so that the Asterisk-Java library knows we want to receive events. That means that we have to provide an “onManagerEvent” method.
Our implementation of onManagerEvent is pretty simplistic – we just check if the event is a PSoftLoginEvent, and whether the user ID in that event matches the one that we just sent. A more robust implementation would use some unique IDs for matching up the exact logins; not just check the user ID. We’d also want to check for other related events such as the user hanging up without even trying to enter the PIN code.
Another thing worth pointing out here is that in the Asterisk-Java library, event notifications come in on a different thread. PeopleCode only runs single threaded, but when you are calling Java from within PeopleCode, there may be multiple threads running from within the Java Virtual Machine.
In this demo implementation, we’re cheating a bit by just sleeping on the main thread while we wait for the login event to come back. It works just fine for our purposes here, but it’s definitely not production ready code. We’d probably also want to have some sort of a connection pool instead of logging in to the Asterisk server on every request.
package com.greysparling.asterisk;
import java.io.IOException;
import java.util.HashMap;
import org.asteriskjava.manager.AuthenticationFailedException;
import org.asteriskjava.manager.ManagerConnection;
import org.asteriskjava.manager.ManagerConnectionFactory;
import org.asteriskjava.manager.ManagerEventListener;
import org.asteriskjava.manager.TimeoutException;
import org.asteriskjava.manager.action.OriginateAction;
import org.asteriskjava.manager.event.ManagerEvent;
import org.asteriskjava.manager.response.ManagerResponse;
public class PSoftLoginManager implements ManagerEventListener {
private ManagerConnection connection;
private boolean keep_running = true;
private String userId;
private PSoftLoginEvent event;
static final String USERID = "PSFTUSERID";
static final String PIN = "PIN";
public PSoftLoginManager(String host, String user, String pswd) throws
AuthenticationFailedException, TimeoutException, IOException {
ManagerConnectionFactory factory =
new ManagerConnectionFactory(host, user, pswd);
connection = factory.createManagerConnection();
connection.addEventListener(this);
connection.registerUserEventClass(PSoftLoginEvent.class);
connection.login();
}
public PSoftLoginEvent challengeUser(String userId, String phone,
String pinCode, String channel, String context, String exten)
throws InterruptedException, TimeoutException, IOException {
this.userId = userId;
this.event = null;
HashMap vars = new HashMap();
vars.put(USERID, userId);
vars.put(PIN, pinCode);
OriginateAction action = new OriginateAction();
action.setChannel(channel + "/" + phone);
action.setContext(context);
action.setVariables(vars);
action.setExten(exten);
action.setPriority(new Integer(1));
ManagerResponse response = connection.sendAction(action, 30000);
System.out.println(response.getResponse());
while (keep_running)
Thread.sleep(100);
return this.event; // caller can check isSuccess()
}
public void onManagerEvent(ManagerEvent event) {
if (event instanceof PSoftLoginEvent) {
PSoftLoginEvent login_event = (PSoftLoginEvent)event;
System.out.println("Login event: " + login_event);
if (userId.equals(login_event.getUserId())) {
System.out.println("Matched user");
this.event = login_event;
this.stop();
}
else {
System.out.println("User was " + login_event.getUserId());
}
}
else {
System.out.println("Event: " + event);
}
}
public void stop() {
keep_running = false;
}
}
So there you have it. We’ve successfully made phone calls from within PeopleSoft, challenged user for a PIN code, and take action accordingly. Everything that we need for two factor authentication of PeopleSoft users, and our total cost is still under one dollar.
(updated with syntax highlighting)
Labels: 2008, Firewall, Java, PeopleCode, Security,
Telephony