So, earlier today I posted a short video about a new feature that I’ve written for IQJam (initially at least, it will be coming to IdeaJam at some point). It allows you to authenticate against a Domino server using Facebook Single Sign On (SSO).
Overall this is not for the faint of heart (or the new XPages developer, hence me not going into detail about how what the code is doing etc), there are a lot of moving parts to get things working properly. That being said there is absolutely no reason exactly the same approach couldn’t be taken with a classic Domino application. All of the server Side Javascript would need to be converted to LotusScript and Java (for the network operations).
1) Register your application with Facebook here: http://www.facebook.com/developers/apps.php
2) In the app home page, add a facebook login graphic with some client side onClick code:
var returl = "http://[Your App Host Name][Your App DB Path]/fblogin.xsp";
var url = "https://graph.facebook.com/oauth/authorize?client_id=[YOUR FACEBOOK APP ID]&redirect_uri=" + returl;
window.open (url, "mywindow","width=500,height=250");
3) Create a new XPage called fblogin
4) In the afterPageLoad event put the following code:
try{
//Get these from Facebook App registration
var API_KEY = "YOUR FACEBOOK APP ID";
var SECRET = "YOUR FACEBOOK SECRET KEY";
//Get the auth code from url param returned by facebook
var code = context.getUrlParameter("code");
//Now swap the auth code for the access_token key
var urltoken = "https://graph.facebook.com/oauth/access_token?client_id=" + API_KEY +
"&redirect_uri=http://[yourhostname]" +
"/" + @ReplaceSubstring(database.getFilePath(), "\\", "/") + "/fblogin.xsp" +
"&client_secret=" + SECRET +
"&code=" + code;
var urltoken:jav.net.URL = new java.net.URL(urltoken);
var urltokenconn = urltoken.openConnection();
var tokenreader = new java.io.BufferedReader(
new java.io.InputStreamReader(
urltokenconn.getInputStream())
);
var inputLine;
var accesstoken = "";
while( (inputLine = tokenreader.readLine()) != null){
accesstoken += inputLine;
}
tokenreader.close();
accesstoken = @Right(accesstoken, "=");
//Now get the user info using the access_token
var url = "https://graph.facebook.com/me?access_token=" + accesstoken + "&client_id=" + API_KEY;
var url:java.net.URL = new java.net.URL(url);
var urlconn:java.net.URLConnection = url.openConnection();
var reader:java.io.BufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(
urlconn.getInputStream())
);
var inputLine;
var userjson = "";
while ((inputLine = reader.readLine()) != null){
userjson += inputLine;
}
reader.close();
//Now we've got a JSON object which contains the user data
userjson = eval("(" + userjson + ")");
var firstname = userjson.first_name;
var lastname = userjson.last_name;
var userId = userjson.id;
var fbname = getFBName(firstname, lastname, userId);
var password = getFBPassword(fbname, SECRET);
print("FBName = " + fbname);
var fbreg = new facebookReg();
if (fbreg.validateUser(fbname.getCanonical())){
//We need to go and register the user
fbreg.registerNewFBUser(firstname, lastname, fbname, password);
}
//Set the username and password fields so the Ajax login can happen
viewScope.username = fbname.getCanonical();
viewScope.password = password;
}catch(e){
_dump(e);
}
5) In a supporting script library you’ll need the following functions:
/*
An object which handles authentication / registration of Facebook users
Created By: Matt White
Date Created: October 2010
Version: 1.0
*/
var facebookReg = function(){
var dbNab:NotesDatabase = null;
var dbMainNab:NotesDatabase = null;
var registerNewFBUser = function(firstname, lastname, fbname, password){
getDbs();
if(validateUser(fbname.getCanonical())){
var registerNewUser = false;
if (!addUserToGroup(fbname)){
print("Couldn't add " + fbname.getAbbreviated() + " to group");
}else{
dbNab.DelayUpdates = false
dbMainNab.DelayUpdates = false
var docPerson = dbNab.createDocument();
docPerson.replaceItemValue("form", "Person");
docPerson.replaceItemValue("Type", "Person");
docPerson.replaceItemValue("LastName", lastname);
docPerson.replaceItemValue("FirstName", firstname);
var item = docPerson.replaceItemValue("FullName","");
item.appendToTextList(fbname.getCanonical());
item.appendToTextList(firstname + " " + lastname);
docPerson.replaceItemValue("HTTPPassword", password);
docPerson.replaceItemValue("accountstatus", "Not Verified");
docPerson.computeWithForm( false, false );
print("Saving new person doc: " + docPerson.getUniversalID() + " in " + dbNab.getTitle());
docPerson.save();
var addviews = new Array();
addviews.push(dbNab.getView("($LDAPCN)"));
addviews.push(dbNab.getView("($Users)"));
addviews.push(dbNab.getView("($ServerAccess)"));
addviews.push(dbNab.getView("($VIMPeople)"));
addviews.push(dbMainNab.getView("($ServerAccess)"));
addviews.push(dbMainNab.getView("($VIMGroups)"));
addviews.push(dbMainNab.getView("($Users)"));
for(var i=0; i<addviews.length; i++)
addviews[i].refresh();
print("Refreshed views");
//Finally create a profile document for the person
var dbCurrent = sessionAsSigner.getDatabase(database.getServer(), database.getFilePath());
var profile = dbCurrent.createDocument();
profile.replaceItemValue("Form", "person");
profile.replaceItemValue("Name", fbname.getCanonical());
profile.computeWithForm(false, false);
profile.save();
print("Created profile document");
}
}
}
var validateUser = function(thisname){
getDbs();
var people = dbNab.getView("($Users)");
var collection = people.getAllDocumentsByKey(thisname, true);
print("Found " + collection.getCount() + " matching people for " + thisname);
if (collection.getCount() > 0)
return false;
else
return true;
}
var addUserToGroup = function(nname){
var group = "[Your Group Name Here]";
var groups = dbMainNab.getView("Groups");
var docGroup = groups.getDocumentByKey(group, true);
if (docGroup == null){
docGroup = dbMainNab.createDocument();
docGroup.replaceItemValue("Form", "Group");
docGroup.replaceItemValue("ListName", group);
docGroup.replaceItemValue("Members", group & " 1");
docGroup.replaceItemValue("GroupType", "0");
docGroup.replaceItemValue("ListDescription", "Do NOT edit this group manually, it is updated via an agent!!!");
docGroup.computeWithForm( false, false );
docGroup.save();
}
var groupMainMembers = docGroup.getFirstItem( "Members" );
var subGroup = "";
for (var x=groupMainMembers.getValues().length; i>=0; i--){
if (@Left(groupMainMembers.getValues()[x], @Length( group )) == group)
subGroup = groupMainMembers.getValues()[x];
}
groupNum = 0;
if (subGroup != "")
groupNum = @TextToNumber( @Right( subGroup, @Length( subGroup ) - @Length( group ) - 1 ) );
else{
groupNum = 1
subGroup = group + " 1";
}
while(true){
var groupSubDoc = groups.getDocumentByKey( subGroup, true );
if (groupSubDoc == null){
groupSubDoc = dbMainNab.createDocument();
groupSubDoc.replaceItemValue("Form", "Group");
groupSubDoc.replaceItemValue("ListName", subGroup);
groupSubDoc.replaceItemValue("GroupType", "0");
groupSubDoc.computeWithForm( false, false );
if (!groupMainMembers.containsValue( subGroup )){
try{
groupMainMembers = docGroup.getFirstItem("Members");
groupMainMembers.appendToTextList(subGroup);
saveGroupMainDoc = true;
}catch(e){
_dump(e);
}
}
}
var groupSubMembers = groupSubDoc.getFirstItem( "Members" );
if (groupSubMembers.getValueLength() < 10000)
break;
groupNum = groupNum + 1;
subGroup = group + " " + groupNum;
}
groupSubMembers.appendToTextList(nname.getCanonical());
groupSubDoc.save( false, true );
docGroup.save( false, true );
return true;
}
var getDbs = function(){
if (dbNab == null || dbMainNab == null){
dbNab = sessionAsSigner.getDatabase(database.getServer(), "[NAB Where Users Are Stored]");
dbMainNab = sessionAsSigner.getDatabase(database.getServer(), "[Main NAB]");
}
}
return {
// public methods
registerNewFBUser: registerNewFBUser,
validateUser: validateUser,
addUserToGroup: addUserToGroup,
getDbs: getDbs
}
}
/*
Creates a new Notes Name using First Name, Last Name, Facebook User ID
*/
function getFBName(firstname, lastname, uid){
return session.createName(firstname + " " + lastname + "/" + uid + "/Facebook");
}
/*
Generates a password using a Notes Name and a secret key as a salt
*/
function getFBPassword(fbname:NotesName, seed){
var result = session.evaluate("@Password(\"" + fbname.getCanonical() + seed + "\")");
return @ReplaceSubstring(result.elementAt(0), ["(",")"], "");
}
6) Finally you’ll need some AJAX code which logs the user in assuming all of the previous code has worked properly
function doLogin(userNameId, passwordId, facebookmode){
dojo.xhrPost({
url: "/names.nsf?login",
content: {
username: dojo.byId(userNameId).value,
password: dojo.byId(passwordId).value,
redirectto: dbPath + "/username.txt?open&rnd=" + Math.random()
},
load: function(data) {
try {
if( data.indexOf("Anonymous") == -1) {
dojo.byId("loginMsg").style.display = "block";
dojo.byId("loginMsg").style.color = "green";
dojo.byId("loginMsg").style.backgroundColor = "transparent";
dojo.byId("loginMsg").innerHTML = "Please Wait";
if(location.href.indexOf("register.xsp") > -1){
location.href = dbPath;
}else{
if (facebookmode == true){
window.opener.location.href = window.opener.location.href;
window.close();
}else{
if (window.location.href.indexOf("#") > -1){
window.location.replace( strLeft(window.location.href, "#") );
}else{
window.location.replace( window.location.href );
}
}
}
} else {
dojo.byId("loginMsg").style.display = "block";
dojo.byId("loginMsg").style.color = "red";
dojo.byId("loginMsg").style.backgroundColor = "transparent";
if ( dojo.cookie('DomAuthSessId') != null || dojo.cookie('LtpaToken') != null ) {
dojo.byId("loginMsg").innerHTML = "You do not have access to this database";
} else {
dojo.byId("loginMsg").innerHTML = "Wrong username or password";
}
}
}catch(e){
alert(e);
console.error ('Error: ', error);
}
},
error: function(data) {
alert(e);
console.error ('Error: ', error);
}
});
}
7) That code will need to be triggered by some Javascript which runs when the page loads (so this needs to go in the onClientLoad function)
if (gup("code") != ""){
doLogin("#{id:username}", "#{id:password}", true)
}else{
window.close();
}
For the gup function go here: http://www.netlobo.com/url_query_string_javascript.html
The integration with facebook is an awsome step to make xpages even more nice. I think it would be possible to do the same with for instance twitter?
Ah – doing the AJAX HTTP Post to perform the login is a nice touch saves having to create the token.
Thanks for posting this Matt.
Thanks for sharing Matt, perhaps plublish this on openntf.org as a demo app would be great next step.
nice one, Matt.
I always wondered how one could create SSO solutions for Domino using "foreign" authentication, but it makes sense the way you did it.
What puzzles me is that it seems you can read the user's password from his facebook account (since you create a new person document and writing that password into the httppassword field)?
Or is this some kind of generated password, created by the facebook system?
Matt, can I suggest adding the following to your registerNewFBUser function?
docPerson.replaceItemValue("$SecurePassword", "2");
Theoretically the .computeWithForm call will enforce whatever password storage policy you've set in the directory, but I've personally found it a good idea to enforce the salted storage as much as possible.
Nice use of the fb uid as a custom OU there. Very slick. đŸ˜‰
sorry for my comment, I didn't read until the end.. đŸ™‚ Now I know where the password's coming from.
Hi Matt,
Thanks for the wonderful code.
I am facing issue with sessionAsSigner in afterPageLoad event. Server is 8.5.2, still giving error: sessionAsSigner not found. It works if I use sessionAsSigner within onClick event of a button but not with XPage events.
Please suggest if anyone else has faced such issue ?
Check out real authentication using http://simply-sso.com. No need to handel domino passwords…