03 July 2008

SharePoint and Oracle Internet Directory (LDAP)

[UPDATE 6 SEPT 2010] The code has been posted on a new article about connecting SharePoint to Oracle LDAP

I have a client who recently wanted to leverage their Oracle Internet Directory server to authenticate users in SharePoint.  Since OID is LDAP compliant, I figured I could just use the generic LDAP provider with SharePoint.  Turns out, that provider is only supplied with MOSS and my client was using WSS.  So, off to Visual Studio to create a custom membership provider I went. 

I've seen a number of posts regarding the creation of custom membership providers for non-Microsoft directory sources like SunOne and OID.  However, I never saw any code samples (most folks didn't seem to be able to get it to work).  While other posts on custom membership providers tended to rehash the same material on SQL Membership.  As it turns out, it's pretty straight forward, as long as you have all of the right connection information (and understand the schema of the directory).  I would be happy to post the code if anyone is interested.  Short of that, here are some of the discoveries I've made:

  1. You can use the standard System.DirectoryServices.Protocols.LdapConnection class to connect to OID.  Here's the generic code I used:

    LdapDirectoryIdentifier _identifier = new LdapDirectoryIdentifier(ldapServerName, portNumber);
    connection = new System.DirectoryServices.Protocols.LdapConnection(_identifier);

  2. You need to override the Initialize member of the base membership class to supply the various connection settings you'll need.  There's a NameValue collection that's passed to this member built from the properties on the provider information you have to supply in Web.Config.  In my case, I limited my settings to LDAP server address (name or IP address), the LDAP port (originally my client suggested they used a non-standard port), Application Name (a property you must override when developing a custom membership provider) and LDAP domain (e.g. dc=com,  dc=customer).   The MOSS supplied provider is a bit more flexible, but I didn't particularly need all of that flexibility (read "complexity").
  3. I had to add the same provider information to Central Administration as I did for my SharePoint site.  I didn't change the authentication method on Central Admin, but without adding the provider, I could not get the people picker (in my SharePoint site) to resolve the user IDs from OID.  Once I added the provider all worked well.  In fact, in some cases, it seems that Central Admin "knows" about the user before the SharePoint application.
  4. You have to overload the following members in the base membership provider in order for it to work with SharePoint
    1. ValidateUser (the member that actually ensures the user is who they say they are)
    2. GetUser (both versions)
    3. GetUserNameByEMail
    4. FindUsersByName
    5. GetAllUsers

      An article published on MSDN defines the minimum interfaces: http://msdn.microsoft.com/en-us/library/bb975135.aspx#MOSSFBAPart2_DevelopingCustom
  5. You don't necessarily have to authenticate with the directory to exercise most of the members above, with the exception of ValidateUser.  In my case, my client's OID implementation enabled anonymous access.
  6. The act of "binding" to the LDAP directory validated the user.  That said, this only worked for users in the directory.  If I passed a user that did exist, but provided the wrong password, the validation failed as expected.  If, however, I passed a non-existent user to the directory (regardless of the combination of user ID and password), the Bind() method did not return any errors and my "authentication" worked.    I'm not sure if I've missed something or if this is unique to LDAP, but it was a strange behavior.  During my development I used a tool called LDAP Admin (http://ldapadmin.sourceforge.net/).  The tool was invaluable for validating connection information and user data.  It also, however, suffered the same fate as my code -if I tried to authenticate a user that didn't exist, the connection would be successful.  Weird.
  7. My client also wanted to use a single sign-on service provided by OID.  The service is quite flexible, supporting three different methods for supplying credentials to target applications - GET, POST and BASIC..  Unfortunately, since we configured the site with a custom membership provider, the only options were either GET or POST.  We chose post.  Unfortunately, the standard login page doesn't seem to want to allow a post from another page (although I didn't spend a long time to work it out).  I ended up constructing a basic page that took in form properties and then used the Membership class to authenticate the user and redirect them back to the SharePoint site.

Ultimately, the new provider works well and the SharePoint site is integrated with the SSO server.  User's who have access to my client's Oracle Portal can seamlessly access their SharePoint site as well  -- all without having to authenticate a second time.  And, because both web sites use the same LDAP, there's not need to synchronize user IDs and passwords.

Again, if you're interested in the code, just drop me a line; if I get enough requests, I'll post the code directly.

[UPDATE 6 SEPT 2010] The code has been posted on a new article about connecting SharePoint to Oracle LDAP

12 comments:

Eric said...

I would love to see some source for this... We're currently trying to authenticate against an OID server. It used to be an Active Directory server which using the built in ActiveDirectoryMembershipProvider worked slick. We have our own custom LDAPAuthentication library which uses DirectoryServices to authenticate a user, but it's currently not working (in the middle of trying a few different settings first) but to use the built in mechanisms would be fantastic. Good post, btw.

Shawn said...

Eric:

Send your contact information to: shawn_shell@consejoinc.com and I'll get you the code (or we could have a quick conversation).

Shawn

Anonymous said...

Dear shawn,

Nice post ..It would be great if you have posted the source code,Can you please do that..cos i have been trying to work this.
thanks in advance,

Sivabalan /
sivabalan11@hotmail.com

Anonymous said...

Hi Shawn,
Thank you for your post!
I've been looking for a membership provider to connect WSS using FBA to an existing ORACLE database. I don't have much more information about the ORACLE database as of today, but I'm very interested in your code.
If you would be so kind in sending me your code (vmaximiuk@gmail.com), I would appreciate it greatly.
Thank you in advance.
Vee

Ivan said...

Shawn! It's the same challenge I'm worried about now - possiblity of SPS users be authenticated thru OID and SSO. I'm mostly oracle-educated analyst and had nothing to do with MOSS,WSS and etc. so your codes would be very handy.
My email is ivanievlev@gmail.com
Have a good blogging!

Ivan said...

Shawn! It's the same challenge I'm worried about now - possiblity of SPS users be authenticated thru OID and SSO. I'm mostly oracle-educated analyst and had nothing to do with MOSS,WSS and etc. so your codes would be very handy.
My email is ivanievlev@gmail.com
Have a good blogging!

Anonymous said...

I would appreciate he you have a code to read the tsname description inside the OID. (LDAP)

This is the problem.
In the WEB to communicate with Oracle we need to bring a statement like

Public Shared Function DBConnect(ByVal strUser As String, ByVal strPass As String) As OracleConnection
Dim Strconn, c1 As String
Dim cn As OracleConnection
cn = New OracleConnection
Strconn = "User ID=" & strUser
Strconn = Strconn & ";password=" & strPass

Strconn = Strconn & ";Data Source=(DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = sxxxxx.miamidade.gov)(PORT = 60020))"
Strconn = Strconn & " (ADDRESS = (PROTOCOL = TCP)(HOST = sxxxxx.miamidade.gov)(PORT = 60020))"
Strconn = Strconn & " (LOAD_BALANCE = yes)) "
Strconn = Strconn & " (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = Tdpz.miamidade.gov)))"
Strconn = Strconn & " ;Persist Security Info=False"

cn.ConnectionString = Strconn
Try
cn.Open()
c1 = "Connected to Oracle" + cn.ServerVersion
Catch f As OracleException
cn.Close()
End Try
Return (cn)
End Function


Thank you


Gustavo E Lannes

lannesg@miamidade.gov

Vito said...

help me ... can you send me your code
i thing that your job can be so usefull for me, help me your code ;) mercì

vcagnazzo @ gmail.com

Vito said...

thank you so much ;)

For the other users. You can find the KellermanSoftware.NetEncryptionLibrary
to this link
http://www.kellermansoftware.com/download/CRYPT-V1-SD/NET-Encryption-Library.zip

tosaurav said...

Hi Shawn,

I am trying to use changePassword method aginst OID but it's implementaion failing.Can you help me in that?

Shawn said...

@tosaurav... Implementing the ChangePassword bit within the membership provider is really about interacting with OID directly. That wasn't a requirement on my project, but if you send me specific questions, I may be able to point you in the right direction.

Shawn

tosaurav said...

Here is my implementation,I am using MOSS provider,see I am forced to use Authentication Type as Anonymous as all others throws exception

string path = "LDAP://xxxxx.xxx.xxx.xxx:4032/cn=Users,dc=xx,dc=xxx";
DirectoryEntry de = new DirectoryEntry(path, username, _oldpassword.Text, AuthenticationTypes.Anonymous);
if (!de.NativeObject.Equals(null))
{
DirectorySearcher ds = new DirectorySearcher(de);
try
{
ds.Filter = "(&(objectClass=orcluser)(uid=" + username + "))";
ds.SearchScope = SearchScope.Subtree;
SearchResult results = ds.FindOne();

if(results!=null)
{
DirectoryEntry objLoginEntry = new DirectoryEntry(results.Path, username, _oldPassword.Text, AuthenticationTypes.Anonymous);
if (objLoginEntry != null)
{
object obj = objLoginEntry.Invoke("ChangePassword", new Object[] {_oldpassword.Text, _newpassword.Text });
objLoginEntry.CommitChanges();
//obj = null;
//de.Invoke("ChangePassword", new Object[] { _oldpassword.Text, _newpassword.Text });
//de.CommitChanges();

}
}
results = null;
ds = null;
}
catch (Exception ex)
{
_labelmsg.Text = ex.Message.ToString();
if (log.IsErrorEnabled)
{
//log.Error(" ERROR IN OID : " + ex.Message);
log.Info(ex.StackTrace+"/n");
log.Info("@@@@@@@@@ INNER EXCEPTION @@@@@@@@"+ex.InnerException.Message);
log.Info(" EXCEPTION DETAILS " + ex.ToString());
}
}
de = null;
}
else
{
log.Info("LDAP Bind Failed");
}