XPages

How to get SSO for Facebook working with XPages

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