Tyler Muth’s Blog

Technology with a focus on Oracle, Application Express and Linux

PL/SQL LDAP over SSL – Please Test

Posted by Tyler Muth on January 30, 2009

I’ve been meaning to write this for quite a while now (measured in years), but never got around to it.  None of the DBMS_LDAP code samples support SSL, including the APEX LDAP Authentication Scheme.  Most LDAP directories require SSL for authentication, as you would otherwise send usernames and passwords in clear text.  Depending on your network topography, the risk of someone capturing that data might be very low, but just the same, it should really be SSL. 

One perceived barrier to making this work was my belief that this required an Oracle Wallet which are a bit of a hassle to setup.  There was also some confusion on my part that Oracle Wallets required a license for the Advanced Security Option (ASO), so this would limit the use of this code to only the subset of our customers that have purchased this option.  I’m still looking for the email on this but I believe we confirmed that the Oracle Wallet Manger by itself (such as for DBMS_LDAP or UTL_HTTP) does not require ASO.  Only when you are using the explicitly defined features of ASO such as encrypted column / tablespaces / backups, network encryption, and a few others I can’t remember.  If someone has more info on this, please feel free to comment as I’m too [ busy | lazy | (select adjective from adjectives where id = round(dbms_random.value(1, 100)))] to find it [today | tomorrow | (select sysdate + round(dbms_random.value(1,365)) from dual) ].  When I finally sat down to write this for my current project, I quickly discovered that for SSL Mode 1 (which is what 99.999% of people want), no wallet is required!  Can’t believe I put this off for so long based on a false assumption.

So what does this code do? It can:

  • Bind to an LDAP directory using SSL or non-SSL
  • Authenticate a user using their exact Distinguished Name (DN) such as: cn=tyler.muth, l=amer, dc=mycompany, dc=com
  • Given a starting point (search base) and a username (tyler.muth), search for that user, retrieve their DN, then authenticate.  This handles users in multiple sub-trees.

This is NOT production code! This is NOT production code! This is NOT production code!  Why am I posting it here then?  Because I want people to test it.  I’ve tested it on the project I’m working on, but that doesn’t mean it’s bug free.  I want people who are familiar with PL/SQL and LDAP to review and test my code.  As I receive suggested changes to the code, I’ll update it.  When I feel comfortable that enough people have tested, I’ll blog again and “release it” to the community to use.  My goal is make this as flexible and easy as possible so it just becomes a utility that people can use when they need LDAP.  I can’t make any promises, but if it makes it back into APEX, then you will have helped to improve a product.  If you are not all that familiar with PL/SQL and LDAP, or are looking for a production ready solution, this is probably not for you.  When it’s ready, this blog post will be mostly empty and point to the new location of the code.

I’ve only tested this against OID using an 11.something database, so I’d love to have people test this against Active Directory and any other LDAP directories they work with and at least 10gR2 and XE.  Mark Wilcox is testing it with Oracle Virtual Directory in the next few days as well.  I also ran Wireshark on the database server to capture the communitcation between the database and the LDAP directory.  As expected, in non-ssl mode, my username and password were in the clear.  With SSL enabled, everything was garbage.   Please use Wireshark with caution as it may violate network policies of your organization, get you fired, and accelerate global warming.  It’s a great tool when used ethicly.

I really appreciate the feedback I’ve recieved in the past from this community, especially in pointing out errors with my code.  Thanks in advance for your help.

Customize the ldap_globals package with data from your environment. For most environments, you’ll only need to change g_host and g_search_base.

create or replace package ldap_globals
authid definer
is

	g_host         	constant varchar2(255)   := 'ldap.mycompany.com';
    g_port          constant number          := 636;
    g_search_base   constant varchar2(255)   := 'dc=mycompany,dc=com';
    g_search_filter constant varchar2(255)   := 'cn=';
	g_exact_dn      constant boolean   		 := false;
	g_use_ssl       constant boolean   		 := true;

	-- if g_exact_dn is false, then we will start at the search base and find the user to get their DN.
	--   if true then we will just bind with the DN and you should pass the fully qualified user DN into p_username

	-- SSL is hard-coded to use mode 1, as modes 2 and 3 are highly unlikely.  See the DBMS_LDAP doc for more info.
	-- No wallet is needed for mode 1

end ldap_globals;
/
show errors

There’s no need (that I know of) to customize any code in this function.

create or replace function  ldap_authenticate(
	p_username        in    varchar2,
	p_password        in    varchar2)
return boolean
authid definer
is
	l_retval      pls_integer;
	l_session     dbms_ldap.session;
	l_attrs       dbms_ldap.string_collection;
	l_message     dbms_ldap.message;
	l_entry       dbms_ldap.message;
	l_dn          varchar2(1000);
	l_user_handle dbms_ldap_utl.handle;
	l_return      pls_integer;
	l_user_type   pls_integer;

	l_filter      varchar2(256);

	function do_connect
		return pls_integer
	as
		l_ldap_user   varchar2(256) := NULL;
		l_ldap_pwd    varchar2(256) := NULL;
	begin
		dbms_ldap.use_exception := true;
		l_session := dbms_ldap.init( ldap_globals.g_host , ldap_globals.g_port);

    if ldap_globals.g_use_ssl = true then
      l_retval := DBMS_LDAP.open_ssl(
					ld 				=> l_session,
                    sslwrl			=> NULL, -- wallet location, ie file:/etc/ORACLE/WALLETS/oracle
                    sslwalletpasswd	=> NULL, -- wallet password
                    sslauth			=> 1); -- NO_AUTH :1, ONE_WAY_AUTH: 2, TWO_WAY_AUTH: 3
    end if; 

		return dbms_ldap.simple_bind_s( l_session, l_ldap_user, l_ldap_pwd );
	end;

	procedure do_close
	as
	begin
		l_retval := dbms_ldap.unbind_s( l_session );
	end do_close;

	procedure handle_error(p_message in varchar2)
	as
	begin
		do_close;

		$IF $$logger $THEN
			logger.log_error(p_message);
		$END

		RAISE_APPLICATION_ERROR (-20001,p_message);
	end;

begin
	l_user_type := DBMS_LDAP_UTL.AUTH_SIMPLE;

	l_retval := do_connect;

	if ldap_globals.g_exact_dn = false then

		l_attrs(1)  := 'dn';
    l_filter := '('|| ldap_globals.g_search_filter|| p_username||')';
		l_retval := dbms_ldap.search_s( l_session, ldap_globals.g_search_base, dbms_ldap.scope_subtree, l_filter, l_attrs, 0, l_message );

		l_retval := DBMS_LDAP.count_entries(l_session, l_message);

		if l_retval > 1 then
			handle_error('More than one entry found when searching for users DN.');
		end if;

		if l_retval = 0 then
			handle_error('No entries were found when searching for user DN');
		end if;

		l_entry := dbms_ldap.first_entry( l_session, l_message );
		l_dn := dbms_ldap.get_dn( l_session, l_entry );
	else
		l_dn := p_username;
	end if; -- ldap_globals.g_exact_dn = false

	l_retval := null;
	l_retval := DBMS_LDAP_UTL.create_user_handle(
				user_hd     => l_user_handle,
				user_type   => DBMS_LDAP_UTL.TYPE_DN,
				user_id     => l_dn);

		begin
      dbms_ldap.use_exception := false;
      l_return := null;
			l_return := dbms_ldap.simple_bind_s(
				ld      => l_session,
				dn      => l_dn,
				passwd  => p_password
				);
      if l_return != 0 then
        do_close;
        return false;
      end if;

		exception when others then
			do_close;
			raise;
		end;

    dbms_ldap.use_exception := true;
	do_close;
	if l_return is null then
		handle_error('simple_bind_s did not return a value.');
		return false;
	end if;

	if l_return = 0 then
		return true;
	end if;

	exception when others then
		do_close;
		raise;
		return false;
end ldap_authenticate;
/
show errors

A simple test script. If using SQL Developer, make sure you enable output on the DBMS_OUTPUT tab.

set serveroutput on
declare
	l_return boolean;
begin
	l_return := ldap_authenticate(
					p_username	=> 'first.last',
					p_password	=> '');

	if l_return = true then
		dbms_output.put_line('Authenticated');
	else
		dbms_output.put_line('Fail!');
	end;
end;
/

31 Responses to “PL/SQL LDAP over SSL – Please Test”

  1. Dan said

    Tyler,

    I’ll try to give this a go early next week.

    Regards,
    Dan

  2. Salim said

    Hi,

    I will test this against active directory next week and i will give you my feedback

  3. Joseph Charpak said

    Nitpicking: You’ve got two lines of code that do “if boolean = true then…” or “if boolean = false then…”

    You don’t need to do that. you can write “if boolean then…” or “if not boolean then…” I also recommend rewriting the boolean function name or variable to clearly indicate it’s a boolean.

    E.g.
    if ldap_globals.g_exact_dn = false then
    could become:
    if not ldap_globals.g_using_exact_dn then

    and
    if ldap_globals.g_use_ssl = true then
    could become:
    if ldap_globals.g_using_ssl then

  4. Phil Clifford said

    morphic resonance : just getting around to this myself “after several years” and google reader popped this up for me. Will be testing against oid and AD on 10gR2 and XE in a few days – meantime a slight correction :”None of the DBMS_LDAP code samples support SSL” – except the one on (see sslbind.sql in the DBMS_LDAP API package).

  5. Tyler Muth said

    Phil,

    What I meant was the practical LDAP code snippets floating around the APEX forum and various blogs. I actually used the sample code you linked to when I wrote this code and that code has been around for years. It’s not all that useful in it’s current form, but it does do SSL.

    Can’t wait to hear your results from AD. I’ve tested this on 10.2, 11.1 and 11.2, but only against OID and OVD.

    Thanks,
    Tyler

  6. John Scott said

    Hi Tyler,

    Great post, I’ll test it against AD, OID, Sun Directory Server and also OpenLDAP when I get a sec.

    John.

  7. Pat Miller said

    I would appreciate any feedback from other users who have succeeded with SunOne ldap.

  8. Sriram said

    Does not seem to work for 10.2.0.3 and AD combination.


    declare
    l_session dbms_ldap.session;
    l_retval pls_integer;
    begin
    dbms_ldap.use_exception := true;
    l_session := dbms_ldap.init( 'my_domain_controller.domain', 389 );
    l_retval := dbms_ldap.simple_bind_s( l_session, 'domain\user', 'password');
    l_retval := dbms_ldap.unbind_s( l_session );
    end;
    /

    PL/SQL procedure successfully completed.

    declare
    l_session dbms_ldap.session;
    l_retval pls_integer;
    begin
    dbms_ldap.use_exception := true;
    l_session := dbms_ldap.init( 'my_domain_controller.domain', 389 );
    l_retval := DBMS_LDAP.open_ssl( ld => l_session,
    sslwrl => NULL,
    sslwalletpasswd => NULL,
    sslauth => 1);
    l_retval := dbms_ldap.simple_bind_s( l_session, 'domain\user', 'password');
    l_retval := dbms_ldap.unbind_s( l_session );
    end;
    /
    declare
    *
    ERROR at line 1:
    ORA-31202: DBMS_LDAP: LDAP client/server error: UnKnown Error Encountered
    ORA-06512: at "SYS.DBMS_SYS_ERROR", line 86
    ORA-06512: at "SYS.DBMS_LDAP", line 1457
    ORA-06512: at "SYS.DBMS_LDAP", line 1234
    ORA-06512: at line 7

  9. Pat Miller said

    We also got similar ORA 31202 error just trying to do an ssl bind with 10.2.0.3 enterprise edition and SunOne LDAP. My suspicion is that the dbms_ldap package does not even support ssl bind to non-OID LDAPs. Can anyone confirm that? Someone on Apex forum suggested running open source stunnel on the Apex server to mediate.

    Pat

  10. Pat Miller said

    Georger, Tyler,
    I see that Georger was the one who suggested stunnel on the Apex forum. From another post there (Apex forum) regarding pl/sql to AD over SSL what is becoming clear are two options:
    1.) You need certificate exchange from LDAP server to Oracle wallet on Apex server in order to have SSL bind from Oracle to 3rd party ldap. So dbms_ldap supports ssl bind to 3rd party if it has the LDAP server certificate.

    2.) You can bypass the certificate exchange if you use stunnel on the Apex server.

    Correct me if I am wrong!

    Pat

  11. Earl said

    I’ve tested this against an AD and also found the call dbms_ldap.open_ssl is failing with the same error mentioned by Sriram.

    I think we need to clarify something here, if nothing but my own understanding. If this is a function lives in the database and the database is being asked to do the LDAP authentication, shouldn’t the wallet referenced here be a wallet on the Oracle db host rather than the Apex web machine (assuming they are different – as in my case)?

  12. Earl said

    And then the follow-on to my earlier point: wouldn’t that mean that the Oracle instance would need to have Oracle Advanced Security in order to be able to talk SSL to other hosts?

    And isn’t this what is leading people down the path of recommending something like stunnel?

  13. Pat Miller said

    We were able to get basic SSL authenticated bind to work with our SunOne LDAP. We had to do following:
    1.) add our LDAP cert to the Oracle wallet on server and
    2.) put that file location into open_ssl function 2nd parameter and the password into 3rd parameter, and sslauth type 1 in 4th parameter.

    A bind with our ’service user’ dn and password succeeded. We are not using advanced security.

    I think we will have to adapt your authentication function since we cannnot do a search until we have done a ’service user’ dn bind as a valid service followed by the search for the dn of the user based on the userid. In our environment, the SunOne LDAP must have registered our Apex service as a valid service dn (e.g., Cold Fusion server has a service dn, Websphere app server has a service dn, etc.).

    Stay tuned.

    Pat

  14. Pat Miller said

    Correction to my last post.
    #2) should be type 2 in 4th parameter of the open_ssl call.

    Pat

  15. Pat Miller said

    Tyler,
    I don’t understand use of: l_ldap_user and l_ldap_pwd in this section of your authentication function:

    function do_connect
    return pls_integer
    as
    l_ldap_user varchar2(256) := NULL;
    l_ldap_pwd varchar2(256) := NULL;
    begin
    dbms_ldap.use_exception := true;
    l_session := dbms_ldap.init( ldap_globals.g_host , ldap_globals.g_port);

    if ldap_globals.g_use_ssl = true then
    l_retval := DBMS_LDAP.open_ssl( l_session, NULL, NULL, 1);
    end if;
    return dbms_ldap.simple_bind_s( l_session, l_ldap_user, l_ldap_pwd );

    Pat

  16. Pat Miller said

    Hi,
    Just succeeded in getting your function to work for us with our SunOne ldap server! Now just have to add it to Apex as our custom Auth function.

    Keys to getting ours to work:
    A.) had to get our SunOne LDAP server cert into the oracle wallet and identify that location in the open_ssl call; so we had a type 2 call — one way auth.
    B.) Our LDAP requires an application dn so we had to first bind with that dn and password so: l_ldap_user varchar2(256)had to be set to our service dn and l_ldap_pwd varchar2(256) had to be set to our dn password. Then the search for user could succeed.
    C.) The last problem I had was I was using the wrong search base in the ldap_globals package since our schema is so different from the exammples I had seen, but our LDAP administrator was very helpgul in this regard and gave me the correct search base for our LDAP. Once that was correct we were able to authenticate!

    Thanks for providing this posting!!

    Pat

  17. Pat Miller said

    Hi,
    We were so happy to get Tyler’s code adapted and working against our SunOne LDAP. Then we upgraded to Apex 3.2 and now we cannot authenticate. The function still works when we run it thru sql*plus or sql developer with some test users. We made no change to the application but now those same test users who we were authenticating are not able to authenticate — we get invalid credentials. Has anyone else upgraded to 3.2 and are using custom LDAP authentication?

    Pat

  18. SE said

    Sriram,

    It didn’t work for AD because domain\user isn’t valid for ldap only kerberose. You still need to find the full DN just like other ldap systems. Also disable SSL initially and make sure it works then. AD can be difficult when it comes to SSL and on port 389 they use TLS also. 636 is LDAPS. By the way this works for Novell E-directory.

  19. SE said

    PAT,

    I read on another forum that there is a bug. The solution was to change the authentication scheme and then re-apply the one you want.

  20. Jeff said

    Hello,
    Does anyone know how the ldap certificate is managed if you do not use the Oracle Wallet? I have a 3rd party secure ldap which we are trying to authenticate against using Tyler’s code but we don’t want to install OID, Oracle Wallet, or Oracle Certificate Authority. I am getting an error on the dbms_ldap.open_ssl call which has session, null for the wallet location and null for the password, the type is 1.
    thanks,
    Jeff

  21. TomS said

    thanks for the code, i needed full dn search by the uid :) by the way, SSL doesn’t work on my 10.2.0.3.0.

  22. Hi All – there is an Oracle Note via Oracles Metalink Site

    Note.215532.1 Support of PL/SQL API ( DBMS_LDAP ) and C API with Third Party LDAP v3 Directory Servers

    having spoken to Oracle in the last few days the 3 different SSL modes
    1,2 and 3. Mode 1 will only wotk with OID, you will need to use mode 2 and 3 for SSL to work with 3rd party direcotories and Oracle will not support issues with 3rd party directories. It does appear AD will work mode 2, and as Pat above mode 2 appears to work with Sun One.

    I will be testing Oracle DB 10.2.0.4 and sun One directory in mode 2 over a load balancer in the coming days and will let you the results.

    Darragh

  23. Hi Pat – I have a question for you please

    what steps did you follow to get the SunOne Ldap Certificate into an oracle wallet ?
    I know the steps for AD but am not familar with the SunOne directory.

    Darragh.

    • Pat Miller said

      Hi, Darragh,
      Regarding wallet and cert for SunOne:
      l_retval := DBMS_LDAP.open_ssl(l_session,’file:/var/opt/ORACLE/WALLETS/oracle’,'xxxxxxxxx’,2);

      We had to load the cert from our SunOne server into the Oracle wallets directory on the server as referenced above in dbms_ldap call. We got the cert from our SunOne ldap administrator. The xxxxxxxx is the wallet password as described by Tyler.

      Pat

      • darragh duffy said

        Hi Pat – thanks for your rely – one or two questions if you dont mind:

        1. when you loaded the cert from sun, did you use Oracle Wallet Manager or simple place into oracle_home/wallets/oracle ?

        2. Did you have to get an additional Cert fom Verisifn / Twarte etc. or simply did you just us the one issued by the Sun One Administrartor ?

        I have DBMS_LDAP working without the ldapS part, I have been trying to get the ldapbind.exe command to work first i.e.

        ldapbind -h smusdir.bms.com -p 636 -D “uid=core_sso,ou=nonpeople,o=bms.com” -w -U 2 -W “file:c:\coressl” -P
        but I get the Error Unknown Error –

        and if I get that to work I was going to progress to amending my PLSQL code, did you try the above LDABIND command that comes with Oracle DB / Oracle AS etc?

        thanks in advance for your time, Darragh

      • Pat Miller said

        Answers below each question

        >1. when you loaded the cert from sun, did you use Oracle Wallet >Manager or simple place into oracle_home/wallets/oracle ?

        I believe it was just a matter of copying into the wallets directory, but I will double check.
        The following helpful hint was posted at oracle apex (applies to us even though this one relates to AD):
        “The most important trick is that you have an oracle wallet with the root CA certificates in it so that the SSL bind can validate the certificates issued by your AD servers…. To set up ldap via pl/sql in a new server, we simply copy over our ldap wallet and use the exact same code in each instance. The ldap wallet holds nothing of value, just the root certificate authority certs, which aren’t a secret because they automatically get propagated to every windows machine in our domain.”

        >2. Did you have to get an additional Cert fom Verisifn / Twarte etc. >or simply did you just us the one issued by the Sun One >Administrartor ?
        NO, we just used the cert that was provided us by our SunOne ldap administrator– it was Equifax.

        >I have DBMS_LDAP working without the ldapS part, I have been trying >to get the ldapbind.exe command to work first i.e.

        >ldapbind -h smusdir.bms.com -p 636 -D >“uid=core_sso,ou=nonpeople,o=bms.com” -w -U 2 -W “file:c:\coressl” -P
        >but I get the Error Unknown Error –

        >and if I get that to work I was going to progress to amending my >PLSQL code, did you try the above LDABIND command that comes with >Oracle DB / Oracle AS etc?

        Once we had the certs in place we were able to do secure bind. I don’t think we tried a simple bind since we could not get any response other than verify that we could ping the ldap server. Once the certs were in place we tried command line ldaps call and the LDAP administrator checked logs and could see the bind. Then we proceeded with continued work on Tyler’s code to work with our LDAP. The one key item we did not have working properly initially was the search base we initially used was wrong — our schema (higher ed) is quite different from many of the examples used so we had to make sure our search base and other parameters in ldap_globals package were appropriate for our ldap.

  24. Pat Miller said

    Also, everything is working now in Apex 3.2. Belated thanks to SE concerning the bug. Re-applying the authentication scheme did the trick.

    Pat

  25. Eduardo said

    Hi,

    I’m testing with this function to get authenticated in a Active Directory Scheme but I’m not sure why I need to specified my complete name as username parameter (Benjamin Eduardo Garcia) and not my alias (beg) to get authenticated. Someone of you know why is happening that. I’m a newbie in Active Directory so I don’t know if I’m missing something in the search_base or any other place, the values of global variables are:

    g_host constant varchar2(255) := ‘192.168.78.3′;
    g_port constant number := 389;
    g_search_base constant varchar2(255) := ‘DC=patopur,DC=net’;

    BTW the same happens when I use APEX LDAP authentication scheme, any idea?

    Thanks

    Eduardo

  26. Hi All – I finally Managed to get this to work. The main Issue I had was the certificate seemed to be invalid.

    I also managed to verify that this work by using wireshark, intalled it on the Database Server and filter by ip.dst == 192.168.1.xxx

    works quiet nicely.

    also have this working in Novell, still having issues with Sun One but I think this is the certificate or a networking issue.

    Darragh.

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>