ED-Auth Usage Instructions

Introduction

ED-Auth exists to provide an easy means for applications to do simple PID/password authentication and role based authorization (student, faculty, staff, etc.). The ablility to support LDAPv3 over SSL/TLS (ldaps or ldap with startTLS) is the only thing required for connecting to ED-Auth. See the ED-Auth FAQ for common questions about ED-Auth.

Authentication and Authorization

ED-Auth offers simple PID/password authentication plus the ability to authorize on a user's affiliation with Virginia Tech.

Typical Authentication with ED-Auth

The basic steps to authenticate against ED-Auth are:

  1. Collect PID and credential (password) from the user securely (ED Usage Requirements).
  2. Bind anonymously and search for the uupid (PID) (search base: ou=People,dc=vt,dc=edu)
  3. Retrieve the DN from the entry returned by the search
  4. Perform a simple bind with the DN and credential

Typical Authorization with ED-Auth

The basic steps to authorize against ED-Auth are:

  1. Bind as above
  2. Peform an LDAP compare operation for the DN against the eduPersonAffiliation attribute, or search for the person using a filter that specifies the affiliation (e.g. (eduPersonAffiliation=VT-ACTIVE-MEMBER)), or search for the person and cycle through the eduPersonAffiliations, looking for a match. (compare is recommended)

ED-Auth vs. ED-ID

Middleware provides two directories for authentication and authorization purposes. Before you begin the process of interfacing your system with ED-Auth or ED-ID (or both!), you must first consider which system better suits your needs. Both ED-Auth and ED-ID support authentication and provide authorization data, however, ED-ID provides much more authorization data. Below are some criteria to help you decide which system will work best for you. Please note you may have to ask the vendor of your product whether it has some of the functionality listed below.

Use ED-Auth if:

  • Your application can support LDAP over SSL, or startTLS (an LDAP version 3 function)

AND

  • Your application requires extremely fast authentication responses

OR

  • Your application only requires knowledge of a person's basic affiliation (faculty, staff, student, etc.) for authorization decisions

Use ED-ID if:

  • Your application can support SSL or TLS with client certificates

AND

  • Your application requires more information about a person than simply their affiliation

OR

  • Your application requires access to information about which groups a person is a member of

OR

  • Your application requires that the directory keep an explicit list of people who are authorized to use your service

Some applications may even be able to support the usage of both directories. If this is the case, it is strongly recommended that you use ED-Auth for authentication and use ED-ID for the lookup of data pertaining to a person. This guarantees fast response times on authentication requests and allows access to all the information about a person that has been collected for placement in the directory.

Primary Affiliations vs. Standard Affiliations

The following information is taken from the Person Affiliations Explained document. Please refer to this document for more information.

Primary Affiliations

The eduPersonPrimaryAffiliation attribute is an attribute used to communicate, to other institutions, the most basic affiliation a person has with Virginia Tech. This attribute is used in conjunction with systems like Shibboleth* to allow other universities to make authorization decisions about this person. This attribute should NEVER be used by internal Virginia Tech systems for purposes of authorization, it is strictly meant as an external, to VT, facing attribute.

Standard Affiliations

The eduPersonAffiliation attribute gives all the affiliations a person associated with Virginia Tech has with the university. This attribute is meant to be used by internal applications, and will often be used in authorization logic. It is vitally important to realize that this attribute can, and almost always will, have more than one value, which is a change from the current affiliation tracking systems.

Java Applications

SSL Prerequisites for Java Applications

Required JDK Configuration

The following steps are required for all Java applications that talk to ED-Auth. Only JDK versions 1.5 and greater are supported.

  1. Download the VT Root CA in PEM format
  2. Import this certificate into your cacerts truststore:
    keytool -import -keystore $JAVA_HOME/jre/lib/security/cacerts -file $PATH_TO/cacert.pem
    
    • Note that all SSL negotiations used by this JVM will trust the VT Root CA by default.
    • Alternatively, per-application certificate trust can be configured using the javax.net.ssl.trustStore system property.
  3. Download the policy files for the version of your JVM
  4. Unzip the archive and copy US_export_policy.jar and local_policy.jar to $JAVA_HOME/jre/lib/security

Optional JDK Configuration

The following steps are optional, but may provide additional functionality or better performance in some cases.

Installing the Bouncy Castle Provider

The BC cryptographic provider has a number of additional ciphers compared to the default JSSE provider. For example the AES cipher is provided in 3 flavors (AESEngine, AESFastEngine, AESLightEngine) to allow optimization for performance versus resource consumption, so the use of these features of BC can improve application performance accordingly. Additionally, the implementation of the common ciphers is arguably better.

  1. Download the latest Signed Jar File from: http://www.bouncycastle.org/latest_releases.html
  2. Copy bcprov-jdkXX-XXX.jar to the $JAVA_HOME/jre/lib/ext directory.
  3. Rearrange the security providers in $JAVA-HOME/jre/lib/security/java.security:
    security.provider.1=sun.security.provider.Sun
    security.provider.2=com.sun.crypto.provider.SunJCE
    security.provider.3=sun.security.jgss.SunProvider
    security.provider.4=org.bouncycastle.jce.provider.BouncyCastleProvider
    security.provider.5=com.sun.net.ssl.internal.ssl.Provider
    security.provider.6=com.sun.rsajca.Provider
    
    • If your JRE does not contain one of these providers, do not add it.
    • Note that the BouncyCastle provider must be before the Sun SSL provider and the Sun RSA provider, but after the Sun Security provider.

Debugging

If problems persist after following these instructions it is useful to verify that your JVM is configured properly.
To do so, add the following switch when starting the JVM:

java -Djavax.net.debug=ssl ...

This will provide a trace of the SSL negoitation and verification of the keystores that the JVM is configured to use.
This switch is not recommmended for production use.

Using ED-Auth with Applications that Use JNDI

The following example uses JNDI to communicate with ED-Auth over an explicit TLS connection.

Download ED-Auth JNDI example

import java.io.IOException;
import java.util.Hashtable;
import java.util.ArrayList;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.StartTlsRequest;
import javax.naming.ldap.StartTlsResponse;
 
public class EdAuthTlsExample
{
  public static void main(String[] args) {
    if (args.length < 2) {
      System.err.println("USAGE: EdAuthSslExample uupid pass");
      return;
    }
    String hostName = "ldap://authn.directory.vt.edu";
    String baseDn = "ou=People,dc=vt,dc=edu";
    String pid = args[0];
    String credential = args[1];
 
    // Set up JNDI context for an anonymous search for uupid
    Hashtable env = new Hashtable();
    env.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, hostName);
    env.put("java.naming.ldap.version", "3");
    LdapContext ctx = null;
    StartTlsResponse tls = null;
    try {
      ctx = new InitialLdapContext(env, null);
 
      // Authentication must be performed over a secure channel
      tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
      tls.negotiate();
 
      // Perform anonymous lookup of user DN based on uupid attribute
      BasicAttributes attrs = new BasicAttributes("uupid", pid);
      String[] retAttrs = new String[] { "dn" };
      NamingEnumeration ne1 = ctx.search(baseDn, attrs, retAttrs);
      SearchResult pidSearchResult = (SearchResult) ne1.next();
      if (pidSearchResult == null) {
        System.out.println(pid+" not found in ED-Auth");
        return;
      }
      String userDn = pidSearchResult.getNameInNamespace();
      System.out.println("Found user DN "+userDn);
 
      // Authenticate the user and search for privileged attributes
      // belonging to authenticated user
      ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
      ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDn);
      ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credential);
      ctx.reconnect(null);
      NamingEnumeration ne2 = ctx.search(baseDn, attrs);
      while (ne2.hasMore()) {
        SearchResult sr = (SearchResult) ne2.next();
        NamingEnumeration attributes = sr.getAttributes().getAll();
        while (attributes.hasMore()) {
          Attribute attr = (Attribute) attributes.next();
          System.out.println(attr.getID());
          NamingEnumeration values = attr.getAll();
          while (values.hasMore()) {
            System.out.println("\t" + values.next());
          }
        }
      }
    } catch (IOException e) {
      System.err.println("TLS negotiation error:");
      e.printStackTrace();
    } catch (NamingException e) {
      System.err.println("JNDI error:");
      e.printStackTrace();
    } finally {
      if (tls != null) {
        try {
          // Tear down TLS connection
          tls.close();
        } catch (IOException e) {
          System.err.println("Error tearing down TLS connection.");
        }
      }
      if (ctx != null) {
        try {
          // Close LDAP connection
          ctx.close();
        } catch (NamingException e) {
          System.err.println("Error closing JNDI context.");
        }
      }
    }
  }
}

As an alternative to explicitly managing TLS, for ED-Auth it is sufficient to specify “ssl” for the Context.SECURITY_PROTOCOL initial environment parameter to establish a TLS connection. Since ED-Auth is intended for the authentication/authorization use case where all operations need to be performed over a secure channel, an implicit TLS connection may be preferable.

Using ED-Auth via Middleware's LDAP Library

Requires the Middleware EDLdap Library

Download the Middleware EDLdap example

Line 1 import the EdAuth library
Lines 13-15 initialize variables
Line 17 initialize ED-Auth object
Line 18 authenticate and authorize the user
Lines 19-25 get affiliations and print them out

1  import edu.vt.middleware.ldap.ed.EdAuth;
2
3  /**
4   * <p>
5   * EdAuthLibTest provides a test for the EdAuth class.
6   * </p>
7   */
8  public final class EdAuthLibTest
9  {
10   public void doTest()
11     throws Exception
12   {
13     String uupid = "UUPID";
14     String credential  = "PASSWORD";
15     String filter = "AUTHORIZATION FILTER";
16
17     final EdAuth auth = new EdAuth();
18     if (auth.authenticateAndAuthorize(uupid, credential, filter)) {
19       System.out.println("Primary affiliation: "+
20                          auth.getPrimaryAffiliation(uupid, credential));
21       final String[] affil = auth.getAffiliations(uupid, credential);
22       System.out.println("Affiliations: ");
23       for (int i = 0; i < affil.length; i++) {
24         System.out.println("  "+affil[i]);
25       }
26     } else {
27       System.out.println("Authentication or Authorization failed");
28     }
29   }
30
31   public static void main(final String[] args)
32     throws Exception
33   {
34     final EdAuthLibTest test = new EdAuthLibTest();
35     test.doTest();
36   }
37 }

C/C++ Applications

A C/C++ LDAP library supporting LDAP with the startTLS extension is required in order to connect to ED-Auth. These instructions use the OpenLDAP C LDAP library. You must make sure this library is in your applications library path. Please see Appendix: OpenLDAP and Certificates for information on how to set up your environment for SSL/TLS.

Download C Example

Lines 1,2 include necessary headers
Lines 6-21 initialize variables used to connect and perform operations
Lines 23-28 initialize LDAP connection
Lines 30-37 make the LDAP connection use TLS
Lines 39-51 search for the DN for this user
Lines 53-65 get the DN from the search result
Lines 67-75 authenticate the user
Lines 77-86 get this person s record
Lines 88-115 retrieve and print the affiliation information for this user
Lines 117-122 determine if the user has a certain affiliation
Lines 124-127 close the LDAP connection and free used memory

Example compilation:

gcc -I/usr/local/openldap/include -L/usr/local/openldap/lib -o ed-auth ed-auth.c -lldap
1   #include <stdio.h>
2   #include <ldap.h>
3
4   int main(int argc, char* argv[])
5   {
6       LDAP* ldap;
7       LDAPMessage *result, *entry;
8       BerElement* ber;
9       char *HOST_NAME = "authn.directory.vt.edu";
10      int PORT_NUMBER = 389;
11      char *PASSWORD = "PASSWORD";
12      char *filter = "(uupid=UUPID)";
13      char  *attrs[] = {"eduPersonPrimaryAffiliation",
14                       "eduPersonAffiliation", NULL};
15      char *base = "ou=People,dc=vt,dc=edu";
16      char *attribute = NULL;
17      char **values = NULL;
18      char *dn = NULL;
19      char *cmpAttr = "eduPersonAffiliation";
20      char *cmpVal  = "VT-ACTIVE-MEMBER";
21      int i, resultCode, version;
22
23      /* Initialize an LDAP connection. */
24      if( (ldap = ldap_init(HOST_NAME, PORT_NUMBER)) == NULL)
25      {
26          perror("ldap_init");
27          return 1;
28      }
29
30      /* Set the version number so that we may use startTLS. */
31      version = LDAP_VERSION3;
32      ldap_set_option(ldap, LDAP_OPT_PROTOCOL_VERSION, &version);
33
34      if(ldap_start_tls_s(ldap, NULL, NULL) != LDAP_SUCCESS)
35      {
36          ldap_perror(ldap, "ldap_start_tls");
37      }
38
39      resultCode = ldap_search_ext_s(ldap, base, LDAP_SCOPE_SUBTREE,
40                                     filter, NULL, 0, NULL, NULL,
41                                     NULL, LDAP_NO_LIMIT, &result);
42      if(resultCode != LDAP_SUCCESS)
43      {
44          /* another way to print errors...
45          fprintf(stderr, "ldap_search_ext_s: %s\n", 
46                  ldap_err2string(resultCode)); 
47          */
48          ldap_perror(ldap, "ldap_search_ext_s");
49          ldap_unbind_ext_s(ldap, NULL, NULL);
50          return 1;
51      }
52
53      entry = ldap_first_entry(ldap, result);
54      if(entry != NULL)
55      {
56          dn = ldap_get_dn(ldap, entry);
57          ldap_msgfree(result);
58      }
59      else
60      {
61          printf("search on filter: %s returned no entries\n", filter);
62          ldap_msgfree(result);
63          ldap_unbind_ext_s(ldap, NULL, NULL);
64          return 1;
65      }
66
67      /* Bind as a user. If PASSWORD is NULL, resultCode will be LDAP_UNWILLING_TO_PERFORM. 
68         Always make sure password is not NULL. */
69      resultCode = ldap_simple_bind_s(ldap, dn, PASSWORD);
70      if(resultCode != LDAP_SUCCESS)
71      {
72          ldap_perror(ldap, "ldap_simple_bind_s");
73          ldap_memfree(dn);
74          return 1;
75      }
76
77      /* Search for the user. */
78      resultCode = ldap_search_ext_s(ldap, dn, LDAP_SCOPE_BASE,
79                                     filter, attrs, 0, NULL, NULL,
80                                     NULL, LDAP_NO_LIMIT, &result);
81      if( resultCode != LDAP_SUCCESS)
82      {
83          ldap_perror(ldap, "ldap_search_ext_s");
84          ldap_memfree(dn);
85          return 1;
86      }
87
88      /* Since we are doing a base search, there should be only one 
89         matching entry */
90      entry = ldap_first_entry(ldap, result);
91      if(entry != NULL)
92      {
93          printf("\ndn: %s\n", dn);
94          /* Iterate through each attribute in the entry. */
95         for( attribute = ldap_first_attribute(ldap, entry, &ber);
96              attribute != NULL;
97              attribute = ldap_next_attribute(ldap, entry, ber))
98         {
99             /*For each attribute, print the name and values.*/
100            values = ldap_get_values(ldap, entry, attribute);
101            if( values != NULL)
102            {
103                for(i = 0; values[i] != NULL; i++)
104                {
105                    printf("%s: %s\n", attribute, values[i]);
106                }
107                ldap_value_free(values);
108            }
109            ldap_memfree(attribute);
110        }
111        if(ber != NULL)
112        {
113            ber_free(ber, 0);
114        }
115     }
116
117     /* see if user has a specific affiliation */
118     resultCode = ldap_compare_s(ldap, dn, cmpAttr, cmpVal);
119     if(resultCode == LDAP_COMPARE_TRUE)
120         printf("ldap_compare_s: %s has %s=%s\n", dn, cmpAttr, cmpVal);
121     else
122         printf("ldap_compare_s: %s does not have %s=%s\n", dn, cmpAttr, cmpVal);
123
124     ldap_msgfree(result);
125     ldap_memfree(dn);
126     ldap_unbind_ext_s(ldap, NULL, NULL);
127     return 0;
128 }

Note that in addition to setting up the OpenLDAP Library for certificates (Appendix: OpenLDAP and Certificates), you can do this in the code directly:

char *cacertfile = "/path/to/cachain.pem";
ldap_set_option(NULL, LDAP_OPT_X_TLS_CACERTFILE, (void *) cacertfile);

WinLDAP C Applications (Windows)

This example uses the native Windows LDAP API (WinLDAP) to connect to ED-Auth. Similar code could probably be compiled as a COM object or DLL for use with .Net or VB.

Download WinLDAP Example

Lines 20-24 include necessary headers
Lines 33-47 initialize variables
Lines 51-57 initialize the LDAP connection
Lines 59-67 use LDAPv3 and SSL
Lines 69-78 connect to ED-Auth
Lines 80-87 search for the UUPID
Lines 89-92 get the DN
Lines 94-100 bind with the supplied credentials
Lines 102-107 determine if the person has the given affiliation
Lines 109-110 clean up

1   /**
2    * winldap-edauth.c
3    * This code is an example of how to connect to ED-Auth,
4    * search for an entry by a person's UUPID, bind as that
5    * UUPID, and then determine if that person is an active
6    * member at VT with the winldap library.  
7    * This illustrates the basic authentication/
8    * authorization ED-Auth is to be used for.
9    *
10   * Notes:  * You must have imported the VTCA chain into the 
11   *           Windows keystore before this code will work properly.
12   *           This is available at http://www.pki.vt.edu/developer/rootca.html, 
13   *           or click on "immediate installation" and run the .exe at
14   *           http://www.pki.vt.edu/download/ie6.html.  This will 
15   *           automatically install the CA for you.
16   *         
17   *         * You must link this against wldap32.lib
18   */
19
20  #include <windows.h>
21  #include <ntldap.h>
23  #include <winldap.h>
24  #include <stdio.h>
25
26  /**
27   * Search for the DN with the supplied UUPID, bind as that DN with the
28   * given credentials, and then determine if the entry has the 
29   * specified affiliation.
30   */
31  int main(int argc, char* argv[])
32  {
33      LDAP* ld = NULL;
34      INT retVal = 0;
35      PCHAR pHost = "authn.directory.vt.edu";
36      int port = 636;
37      char* base = "ou=People,dc=vt,dc=edu";
38      char* filter = "(uupid=UUPID)";
39      LDAPMessage *result, *entry;
40      char *dn;
41      char *pass = "PASSWORD";
42      char *cmpAttr = "eduPersonAffiliation";
43      char *cmpVal  = "VT-ACTIVE-MEMBER";
44
45      ULONG version = LDAP_VERSION3;
46      SecPkgContext_ConnectionInfo sslInfo;
47      LONG lv = 0;
48  
49      printf("\nConnecting to host \"%s\" ...\n",pHost);
50
51      // Create an LDAP session.
52      ld = ldap_sslinit(pHost, port, 1);
53      if (ld == NULL)
54      {
55          printf( "ldap_sslinit failed with 0x%x.\n",GetLastError());
56          return -1;
57      }
58
59      // Specify version 3; the default is version 2.
60      printf("Setting Protocol version to 3.\n");
61      retVal = ldap_set_option(ld,
62                               LDAP_OPT_PROTOCOL_VERSION,
63                               (void*)&version);
64      if (retVal != LDAP_SUCCESS)
65          return 1;
66
67      retVal = ldap_set_option(ld,LDAP_OPT_SSL,LDAP_OPT_ON);
68
69      // Connect to the server.
70      retVal = ldap_connect(ld, NULL);
71
72      if(retVal == LDAP_SUCCESS)
73          printf("ldap_connect succeeded \n");
74      else
75      {
76          printf("ldap_connect failed with 0x%x.\n",retVal);
77          return 1;
78      }
79
80      // Search for the UUPID
81      retVal = ldap_search_s(ld, base, LDAP_SCOPE_SUBTREE, filter, NULL, NULL, &result);
82
83      if(retVal != LDAP_SUCCESS)
84      {
85          printf("ldap_search_s failed with 0x%x.\n",retVal);
86          return 1;
87      }
88 
89      // Get the DN
90      entry = ldap_first_entry(ld, result);
91      dn = ldap_get_dn(ld, entry);
92      ldap_msgfree(result);
93
94      // Bind with current credentials. 
95      printf("Binding with dn %s...\n", dn);
96      retVal = ldap_bind_s(ld,dn,pass,LDAP_AUTH_SIMPLE);
97      if (retVal != LDAP_SUCCESS)
98          printf("Bind failed with 0x%x.\n", retVal);
99      else
100         printf("Bind as %s succeeded.\n", dn);
101
102     // Determine if this person has the affiliation we want
103     retVal = ldap_compare_s(ld, dn, cmpAttr, cmpVal);
104     if(retVal != LDAP_COMPARE_TRUE)
105         printf("ldap_compare_s failed with 0x%x.\n",retVal);
106     else
107         printf("%s == %s", cmpAttr, cmpVal);
108
109    ldap_memfree(dn);
110    ldap_unbind_s(ld);
111    return 0;
112 }

Perl Applications

This document uses the NET::LDAP LDAP module, which in turn requires the IO::Socket::SSL module. If you choose to use a different LDAP module it must be able to support either LDAP over SSL (LDAPS) or LDAP with the startTLS extension.

Download Perl Example

Lines 3 import the Net::LDAP module
Lines 6-12 declare and initialize variables for use
Lines 14 setup connection to LDAP server
Lines 16-19 make connection use TLS
Lines 21-22 search for the user s DN
Lines 24-28 get person s DN from search result
Lines 30-31 authenticate user
Lines 33-36 search for the person s affiliation information
Lines 38-44 get affiliation information out of search result
Lines 49 close LDAP connection

1  #!/usr/bin/perl 
2
3  use Net::LDAP;
4  use strict;
5
6  my $server = "authn.directory.vt.edu";
7  my $port = 389;
8  my $dn = "";
9  my $pass = "PASSWORD";
10 my $cafile = "/PATH/TO/ED_AUTH/CA_file.pem";
11 my $base = "ou=People,dc=vt,dc=edu";
12 my $filter = "(uupid=UUPID)";
13
14 my $ldap = Net::LDAP->new($server, port => $port, version => 3) or die $@;
15
16 my $mesg = $ldap->start_tls(verify => 'require',
17                             cafile => $cafile,
18                             ciphers => 'RC4-SHA');
19 $mesg->code && die $mesg->error;
20
21 $mesg = $ldap->search(base => $base, filter => $filter);
22 $mesg->code && die $mesg->error;
23
24 my $entry = $mesg->entry(0);
25 if($entry)
26 {
27     $dn = $entry->dn;
28 }
29
30 $mesg = $ldap->bind( dn => $dn, password => $pass);
31 $mesg->code && die $mesg->error;
32
33 $mesg = $ldap->search(base => $base, filter => $filter,
34                       attrs => ['eduPersonPrimaryAffiliation',
35                                 'eduPersonAffiliation']);
36 $mesg->code && die $mesg->error;
37
38 $entry = $mesg->entry(0);
39 if($entry)
40 {
41     print $entry->get_value('edupersonprimaryaffiliation')."\n";
42     my $ref = $entry->get_value('edupersonaffiliation', asref => 1);
43     foreach(@{$ref}){ print $_."\n"; }
44 }
45
46 # print all attributes for the entry
47 #foreach $entry ($mesg->all_entries) { $entry->dump; }
48
49 $ldap->unbind;

Python Applications

This example uses python-ldap to communicate with ED-Auth. Since python-ldap is a wrapper around the OpenLDAP libraries, OpenLDAP and OpenSSL are required for this example to work. Certificates can be set up according to Appendix: OpenLDAP and Certificates. See Appendix: Compiling OpenLDAP Libraries for help with compiling the OpenLDAP library. See the INSTALL document bundled with python-ldap for installation instructions (hint: modify setup.cfg to give OpenLDAP and OpenSSL include and library paths).

Download Python Example

Line 3 import python-ldap
Lines 5-10 initialize variables
Line 14 initialize thy LDAP connection
Lines 15-16 startTLS on the connection
Line 17 search for the user
Line 18 get result of search
Lines 20-21 get the DN from the search and bind as that user
Lines 22-25 determine if user has the specified eduPersonAffiliation (authorization)

1  #!/usr/bin/python
2
3  import ldap
4
5  base = "ou=People,dc=vt,dc=edu"
6  scope = ldap.SCOPE_SUBTREE
7  searchFilter = "uupid=UUPID"
8  password = "PASSWORD"
9  attribute = "eduPersonAffiliation";
10 affiliation = "VT-ACTIVE-MEMBER"
11
12 try:
13     # initialize the ldap connection
14     edauth = ldap.initialize("ldap://authn.directory.vt.edu:389")
15     edauth.protocol_version=ldap.VERSION3
16     edauth.start_tls_s()
17     results = edauth.search(base, scope, searchFilter, None)
18     resultType, result_data = edauth.result(results, 0)
19     if result_data:
20         dn = result_data[0][0]
21         edauth.simple_bind_s(dn, password)
22         if edauth.compare_s(dn, attribute, affiliation) == 0:
23             print dn, "does not have", attribute, "=", affiliation
24         else:
25             print dn, "has", attribute, "=", affiliation
26     else:
27         print "UUPID not found"
28 except ldap.LDAPError,e:
29     print str(e)

PHP Applications

This example uses PHP to connect to ED-Auth. You must compile PHP with LDAP for this example to work. See Appendix: Compiling OpenLDAP Libraries for instructions on installing OpenLDAP libraries.

You also need to make sure that certificates are set up properly. See Appendix: OpenLDAP and Certificates for instructions.

Download PHP Example

Lines 3-11 create and initialize variables
Lines 14-24 determine user's DN
Lines 28-41 authenticate and authorize user
Lines 42-53 do a base search on the user's DN and print an attribute (eduPersonAffiliation)

  1 <?php
  2     
  3 $host = 'ldap://authn.directory.vt.edu';
  4 $baseDn = 'ou=People,dc=vt,dc=edu';
  5 $userfield = 'uupid';
  6 $pid = 'UUPID';                                                               
  7 $credential = 'PASSWORD';
  8 $attr = 'eduPersonAffiliation';
  9 $value = 'VT-ACTIVE-MEMBER';
 10 $groupAttr = 'groupMembership';
 11 $group = 'uugid=department.staff,ou=Groups,dc=vt,dc=edu';
 12 
 13 /*ldap will bind anonymously, make sure we have some credentials*/
 14 if (isset($pid) && $pid != '' && isset($credential)) {
 15     $ldap = ldap_connect($host);
 16     ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
 17     if (!@ldap_start_tls($ldap)) {                                           
 18         print('Could not start TLS');
 19     } else if (isset($ldap) && $ldap != '') {
 20         /* search for pid dn */
 21         $result = @ldap_search($ldap, $baseDn, $userfield.'='.$pid, array('dn'));               
 22         if ($result != 0) {
 23             $entries = ldap_get_entries($ldap, $result);                     
 24             $principal = $entries[0]['dn'];
 25             if (isset($principal)) {
 26                     
 27                 /* bind as this user */
 28                 if (@ldap_bind($ldap, $principal, $credential)) {
 29                     print("Authenticate success\n");
 30                     /* determine if the user has the $attr */
 31                     if(@ldap_compare($ldap, $principal, $attr, $value) === true) {              
 32                        print("$principal has $attr = $value\n");
 33                     } else {
 34                        print("$principal does not have $attr = $value\n");
 35                     }   
 36                     /* determine if the user is in the $group */
 37                     if(@ldap_compare($ldap, $principal, $groupAttr, $group) === true) {         
 38                        print("$principal is a member of $group\n");
 39                     } else {
 40                        print("$principal is not a member of $group\n");
 41                     }
 42                     /* Do a base search on the dn to view all
 43                        eduPersonAffilation(s). */
 44                     $r = ldap_read($ldap, $principal, $userfield.'='.$pid);
 45                     $e  = ldap_first_entry($ldap, $r);
 46                     $attrs = ldap_get_attributes($ldap, $e);
 47                     print("$attr: \n");
 48                     for($i = 0; $i < $attrs[$attr]['count'];
 49                         $i++)
 50                     {
 51                         print("\t".$attrs[$attr][$i]."\n");
 52                     }
 53                     ldap_free_result($r);
 54                 } else {
 55                     print('Authenticate failure');
 56                 }
 57             } else {
 58                 print('User not found in LDAP');
 59             }
 60             ldap_free_result($result);
 61         } else {
 62             print('Error occured searching the LDAP');
 63         }
 64         ldap_close($ldap);
 65     } else {
 66         print('Could not connect to LDAP at '.$host);
 67     }
 68 }
 69 ?>

note: if you are testing ldaps you must specify a fully qualified URL:

ldap_connect("ldaps://$hostname:$port");

you cannot use:

ldap_connect($hostname, $port);

See the PHP Documentation for ldap_connect().

Hosting Users

If your website runs on Hosting, you can only test authentication using the https://secure.hosting.vt.edu URL associated with your website.

A note about the magic_quotes_gpc option and web forms

If you have magic_quotes_gpc set to On in your php.ini, people with certain special characters (',”,\, for example) in their passwords will be unable to authenticate, as the characters will be escaped. Either set magic_quotes_gpc to Off or use stripslashes() to ensure that these people can authenticate. Note that Hosting currently has this option On.

PEAR Net_LDAP

Here is a simplistic example that shows how to bind with PEAR Net_LDAP:

Download PHP Example

<?php
 
require_once 'Net/LDAP.php';
 
$host = 'directory.vt.edu';
$port = 389;
$base = 'ou=people,dc=vt,dc=edu';
$uupid = 'UUPID';
$pass = 'PASS';
 
$config = array (
    'basedn'    => $base,
    'host'      => $host,
    'port'      => $port,
    'starttls'  => true);
 
$ldap = Net_LDAP::connect($config);
 
if(Net_LDAP::isError($ldap))
{
    die('Could not connect to LDAP server: '.$ldap->getMessage());
}
 
$filter = "uupid=$uupid";
 
$search = $ldap->search($base, $filter);
 
if(Net_LDAP::isError($search))
{
    die('Could not fetch entry: '.$search->getMessage());
}
 
$entry = $search->entries();
 
if($entry)
{
    $dn = $entry[0]->dn();
 
    $result = $ldap->bind($dn,  $pass);
    if(Net_LDAP::isError($result))
    {
        die($result->getMessage()."\n");
    }
    else
    {
        echo "Authenticate success\n";
    }
}
else
{
    echo "No entries found for uupid = $uupid\n";
}
 
$ldap->done();
?>

Ruby Applications

To use this class to connect to ED-Auth you must have the ruby-ldap module compiled against the OpenLDAP and OpenSSL libraries. See Appendix: Compiling OpenLDAP Libraries.

Download Ruby Example

1  # Simple ED-Auth library; requires ruby-ldap built
2  # against OpenLDAP and OpenSSL.
3  # Sam Stephenson <sams@vt.edu> 2004-06-17
4  # Modified a bit by Brad Tilley < rtilley@vt.edu> 2006-12-13
5
6  require 'ldap'
7
8  class NotAuthenticatedError < Exception
9  end
10
11 class EdAuth
12     HOST, PORT = 'authn.directory.vt.edu', 389
13     DN, FILTER = 'ou=People,dc=vt,dc=edu', '(uupid=%s)'
14
15     def initialize(pid, pass)
16         @ldap = LDAP::SSLConn.new(HOST, PORT, true)
17         @authenticated = false
18         @pid, @pass = pid, pass
19         @dn, @filter = DN, format(FILTER, pid)
20
21         @authenticity = false
22         @pri_affil, @affil = nil, nil
23     end
24 
25     def authenticate
26         return @authenticity if @authenticated
27         begin
28             @ldap.search(@dn, LDAP::LDAP_SCOPE_ONELEVEL,
29                         @filter) {|c| @dn = c.get_dn}
30             @ldap.bind(@dn, @pass)
31             @authenticity = true
32         rescue LDAP::ResultError
33             @authenticity = false
34         ensure
35             @authenticated = true
36         end
37         @authenticity
38     end
39
40     def get_primary_affiliation
41         query if @pri_affil.nil?
42     puts @pri_affil
43         @pri_affil
44     end
45 
46     def get_affiliations
47         query if @affil.nil?
48     puts @affil
49         @affil
50     end
51
52     def close
53         begin
54             @ldap.unbind
55         rescue LDAP::InvalidDataError
56         end
57     end
58 
59     private
60     def query
61         raise NotAuthenticatedError unless @authenticated and
62         @authenticity
63         @ldap.search(@dn, LDAP::LDAP_SCOPE_SUBTREE, @filter,
64                     ['eduPersonPrimaryAffiliation',
65                     'eduPersonAffiliation']) do |entry|
66                     @pri_affil =
67                     entry.get_values('edupersonprimaryaffiliation').shift
68                     @affil = entry.get_values('edupersonaffiliation')
69         end
70     end
71 end
72
73 x = EdAuth.new('your_pid', 'your_password')
74 x.authenticate
75 x.get_primary_affiliation
76 x.get_affiliations
77 x.close

Using ED-Auth via Apache Modules

Please note you must accept PID/password credentials securely. Since these credentials are given to the Apache server using HTTP Basic Auth, this means that all your restricted resources must be served over an SSL (HTTPS) encrypted connection. Failure to do so is a violation of the ED Usage Requirements.

Apache 1.3

These instructions require the OpenSSL, mod_ssl, OpenLDAP, and auth_ldap libraries. Links to these are provided in the resource appendix. You must have these libraries available to Apache for this to work (See Appendix: Compiling OpenLDAP Libraries for help). These instructions do not describe how to compile Apache, OpenSSL, or mod_ssl. Please refer to the documentation included with those individual programs for compilation instructions.

1. Compile Apache with SSL support. Be sure to enable loadable module support.

2. Compile OpenLDAP with SSL/TLS support (Appendix: Compiling OpenLDAP Libraries)

3. Compile auth_ldap. Here is the configuration we use, obviously some of the paths may be different on your machine:

    ./configure --with-apxs=../apache/bin/apxs \\
                --with-ldap-sdk=openldap \\
                --with-sdk-headers=/usr/local/openldap/include \\
                --with-sdk-libs=/usr/local/openldap/lib \\

4. Compile and install via make and make install

5. Add the following directive into your httpd.conf file so that Apache loads the auth_ldap module:

    LoadModule auth_ldap_module libexec/auth_ldap.so

6. Add the following directives, within a <Directory> directive, into your httpd.conf file for Apache:

AuthLDAPURL ldap://authn.directory.vt.edu:389/ou=People,dc=vt,dc=edu?uupid 
AuthLDAPStartTLS on 
AuthType Basic 
AuthName "Virginia Tech ED-Auth (PID/pass)"  
require valid-user

7. Download the VT CA Chain, also available from VT Middleware CA.

8. Add the following line to your ldap.conf file, usually found at $OPENLDAP_HOME/etc/openldap/ldap.conf (see Appendix: OpenLDAP and Certificates for more ways to set up your trusted certificates):

TLS_CACERT /path/to/cert/download/above/cert.pem

Apache 2.0

1. Compile OpenLDAP with SSL/TLS support (Appendix: Compiling OpenLDAP Libraries)

2. Configure Apache with support for LDAP authentication, here is the configuration we use:

CPPFLAGS=-I/path/to/openldap/include \ 
LDFLAGS=-L/path/to/openldap/lib \ 
./configure --enable-ssl \ 
            --with-ldap \ 
            --enable-ldap=shared \ 
            --enable-auth-ldap=shared \ 
            --prefix=/location/to/install/apache

3. Compile and install Apache with make and make install.

4. Add the following configuration to your httpd.conf file:

LDAPTrustedCA /path/to/CA.pem 
LDAPTrustedCAType BASE64_FILE 
 
<Directory  /some/directory > 
    AuthType Basic 
    AuthName "Virginia Tech ED-Auth (PID/pass)"  
    AuthLDAPURL  ldaps://authn.directory.vt.edu:636/ou=People,dc=vt,dc=edu?uupid  
    require valid-user 
</Directory>

Apache 2.2

1. Compile OpenLDAP with SSL/TLS support (Appendix: Compiling OpenLDAP Libraries)

2. Configure Apache with support for LDAP authentication, here is the configuration we use:

CPPFLAGS=-I/path/to/openldap/include \ 
LDFLAGS=-L/path/to/openldap/lib \ 
./configure --enable-ssl \ 
            --with-ldap \ 
            --enable-ldap=shared \ 
            --enable-authnz-ldap=shared \ 
            --prefix=/location/to/install/apache

3. Compile and install Apache with make and make install.

4. Add the following configuration to your httpd.conf file:

LDAPTrustedGlobalCert CA_BASE64 /path/to/CA.pem 
 
<Directory  /some/directory > 
    AuthType Basic
    AuthBasicProvider ldap
    AuthzLDAPAuthoritative Off
    AuthName "Virginia Tech ED-Auth (PID/pass)"  
    AuthLDAPURL  ldaps://authn.directory.vt.edu:636/ou=People,dc=vt,dc=edu?uupid  
    require valid-user 
</Directory>

Falling Through to file-based Authentication

Sometimes it is desirable to use both ED-Auth and some form of local authentication together. The following is an example of how to use Apache's password files.

LDAPTrustedGlobalCert CA_BASE64 /path/to/CA.pem 
 
<Directory  /some/directory > 
    AuthType Basic
    AuthBasicProvider ldap file
    AuthUserFile /path/to/apache.passwd
    AuthzLDAPAuthoritative Off
    AuthName "Virginia Tech ED-Auth (PID/pass)"  
    AuthLDAPURL  ldaps://authn.directory.vt.edu:636/ou=People,dc=vt,dc=edu?uupid  
    require valid-user 
</Directory>

The AuthBasicProvider directive sets the order of auth modules to attempt to use. In this case it uses file-based auth with apache.passwd if the user is not found in the LDAP.

Note: If you reverse the order of the modules in AuthBasicProvider, you will be able to override users that exist in the LDAP in the password file. This is probably not desirable, and you should ensure that the Apache password file is properly protected.

Tomcat Servlet Container Authentication to ED-Auth

The Tomcat Servlet container can be configured to use the EdAuthRealm provided by the Middleware EDLdap Library to provide container-based authentication and authorization.

Integration steps:

  1. Copy the edldap.jar file to a directory on the container classpath ($TOMCAT_HOME/server/lib for Tomcat 5.x, $TOMCAT_HOME/lib for Tomcat 6.x)
  2. Configure the $TOMCAT_HOME/conf/server.xml file similar to the sample below.
  3. Create the file $TOMCAT_HOME/conf/edauth-users.xml that is formatted similarly to tomcat-users.xml; the password attribute for each user is not needed.
  4. Restart Tomcat.

Template server.xml file for configuring an ED-Auth container authentication realm.

<Server port="8005" shutdown="SHUTDOWN">
 
  <Listener className="org.apache.catalina.core.AprLifecycleListener" />
  <Listener className="org.apache.catalina.mbeans.ServerLifecycleListener"
            descriptors="/edu/vt/middleware/ldap/catalina/mbeans/mbeans-descriptors.xml"/>
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.storeconfig.StoreConfigLifecycleListener"/>
 
  <!-- Global JNDI resources -->
  <GlobalNamingResources>
 
    <!-- Add EdAuth Database as global resource -->
    <Resource name="EdAuthDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="EdAuth role database"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/edauth-users.xml" />
 
  </GlobalNamingResources>
 
  <!-- Define the Tomcat Stand-Alone Service -->
  <Service name="Catalina">
 
    <!-- Define a non-SSL HTTP/1.1 Connector on port 8080 -->
    <Connector port="8080" maxHttpHeaderSize="8192"
               maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
               enableLookups="false" redirectPort="8443" acceptCount="100"
               connectionTimeout="20000" disableUploadTimeout="true" />
 
    <!-- Define an AJP 1.3 Connector on port 8009 -->
    <Connector port="8009" 
               enableLookups="false" redirectPort="8443" protocol="AJP/1.3" />
 
    <!-- Define the top level container in our container hierarchy -->
    <Engine name="Catalina" defaultHost="localhost">
 
      <!-- Because this Realm is here, an instance will be shared globally -->
 
      <!-- Add EdAuth Realm to this Engine -->
      <Realm className="edu.vt.middleware.ldap.catalina.realm.EdAuthRealm"/>
 
      <!-- Define the default virtual host
           Note: XML Schema validation will not work with Xerces 2.2.
       -->
      <Host name="localhost" appBase="webapps"
       unpackWARs="true" autoDeploy="true"
       xmlValidation="false" xmlNamespaceAware="false">
 
      </Host>
    </Engine>
  </Service>
</Server>

EdAuthRealm also supports container-based authorization. Simply specify an <auth-constraint> in the <security-constraint> section of the application web.xml to authorize users based on their membership in a given ED group. The following example requires the authenticated user to be a member of the middleware.staff ED group to access protected resources.

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
  version="2.5">
...
  <security-role>
    <description>ED group describing the administrative role.</description>
    <role-name>middleware.staff</role-name>
  </security-role>
...
  <security-constraint>
    <web-resource-collection>
      <web-resource-name>Protected Resources</web-resource-name>
      <url-pattern>/protected/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
      <role-name>middleware.staff</role-name>
    </auth-constraint>
    <user-data-constraint>
      <transport-guarantee>CONFIDENTIAL</transport-guarantee>
    </user-data-constraint>
  </security-constraint>
...
</web-app>

PAM LDAP

PAM LDAP gives Unix and Linux machines the ability to authenticate against an LDAP server such as ED-Auth. PAM is highly tunable and powerful, and allows administrators to determine how services (login, xdm, ssh, etc.) authenticate users. These instructions assume you are compiling from source, but you are free to use your favorite distribution's package at your own risk.

1. Download PAM LDAP.

2. Configure pam_ldap: (see Appendix: Compiling OpenLDAP Libraries for OpenLDAP dependency)

./configure --prefix=/path/to/pam_ldap \
            --with-ldap=/path/to/openldap \
            --with-ldap-lib=openldap

3. In your /etc/ldap.conf1), add the following:

base ou=People,dc=vt,dc=edu
uri ldap://authn.directory.vt.edu
ldap_version 3
pam_login_attribute uupid
ssl start_tls
tls_checkpeer yes
tls_cacertfile /path/to/cachain.pem

3. Download vt-cachain.pem and put the file in the location you specified in step 3.

4. man pam and read up about the pam.conf configuration file, pam.d, and services.

5. Modify your service rules in /etc/pam.d accordingly, making sure the rules do what you think they do (!!!). (hint: the pam.d directory that comes with the pam_ldap distribution is a good place to look at rules. The login rules file works well.)

pGina Windows Authentication

pGina is a replacement for the authentication portion of Windows 2000/XP, and allows Windows users to authenticate against ED-Auth. This is especially useful in a lab or similar enviromment where PID/pass authentication is desired.

  1. Import the VTCA chain into the Windows keystore. This is available at the VT Root CA site, or, alternatively, click on ”immediate installation” and run the .exe at the VT Root CA site. This will automatically install the VTCA chain for you.
  2. Install pGina and the LDAPAuth Plugin.
  3. Start up the pGina Configuration Tool, click on the Plugin tab, and put the path to the LDAPAuth Plugin for the Plugin Path.
  4. Click on Configure and fill in the following:

Appendix: OpenLDAP and Certificates

If the application you are using to connect to ED-ID or ED-Auth uses the OpenLDAP libraries, it may be necessary to set up the certificates for your client. This is done with the following directives: TLS_CACERT, TLS_CERT, and TLS_KEY. TLS_CACERT refers to the certificate chain used to verify the server. TLS_CERT and TLS_KEY refer to the client certificate and private key, respectively, that are used for TLS client authentication (only used for ED-ID). These directives can be set up in the following ways:

Download the VT Middeware CA chain file

1. Add the following to the OpenLDAP library's ldap.conf. This must be the ldap.conf that corresponds to the OpenLDAP library you are using for your application (note that there may be multiple ldap.conf files on your system, but only one will actually be used by a particular OpenLDAP library).

#############
# ldap.conf
TLS_CACERT      /path/to/cachain.pem
### only needed for ED-ID ###
### NOTE:  TLS_CERT and TLS_KEY are user-only attributes, meaning they 
###        cannot be in ldap.conf, but must be in .ldaprc or ldaprc or environment variables
#TLS_CERT                /path/to/cert.pem 
#TLS_KEY         /path/to/key.pem
#############

This sets up certificates system-wide for the OpenLDAP library.

2. Put the above directives in a file called ldaprc in the user's $HOME directory that is using the application. This will override the system-wide settings for the user.

3. Put the above directives in a file called .ldaprc in the user's $HOME directory that is using the application. This will override the system-wide settings for the user.

4. Put the above directives in a file called ldaprc in the user's current working directory ($PWD) that is using the application. This will override the system-wide settings for the user.

5. Set the following environment variables:

export LDAPCONF=/path/to/ldap.conf
OR
export LDAPRC=/path/to/ldaprc
OR
export LDAPTLS_CACERT=/path/to/cachain.pem
export LDAPTLS_CERT=/path/to/cert.pem
export LDAPTLS_KEY=/path/to/key.pem 

These directives are searched for in this order. The last directives set override any previous directives. For more information on this, see the ldap.conf manpage for OpenLDAP.

Appendix: Testing and Debugging with OpenLDAP

The ldapsearch program distributed with OpenLDAP is a useful tool for testing and debugging your custom application, particularly if it uses the OpenLDAP libraries at some level. Below are examples for how to connect to ED-Auth. Please refer to Appendix: OpenLDAP and Certificates for information on how to set up certificates.
NOTE: Your OpenLDAP libraries must be compiled with SSL support to do this.

To search for a person:

ldapsearch -H ldap://authn.directory.vt.edu -x -Z \
    -b ou=People,dc=vt,dc=edu '(uupid=<uupid to search for>)'

To bind as a person:

ldapsearch -H ldap://authn.directory.vt.edu -x -Z \ 
    -b ou=People,dc=vt,dc=edu \ 
    -D uid=<uid to bind as>,ou=People,dc=vt,dc=edu \ 
    -W '(uupid=<uupid to search for>)'

Options Key:

  • -H – specifies the LDAP URL
  • -x – specifies that we want to a simple bind (anonymous in this case)
  • -Z – specifies that we want to do Start TLS request
  • -b – specifies the search base for the search
  • -D – the DN of the user you want to bind as
  • -W – prompts the user for the bind password

Appendix: PHP, SSL, and Windows

To use LDAPS functionality in PHP on Windows you must create the following file:

c:\OpenLDAP\sysconf\ldap.conf

and place the following line in it:

TLS_CACERT c:\OpenLDAP\sysconf\certs\cachain.pem

Next, place the VT CA Chain in the

c:\OpenLDAP\sysconf\certs\

directory.

Appendix: Compiling OpenLDAP Libraries

Many of the examples contained in this document depend on the OpenLDAP LDAP Libraries for their functionality. This includes the C, Net::LDAP, Python, PHP, Ruby, and Apache examples as well as the standard LDAP utilities such as ldapsearch. This appendix exists to help you compile the libraries needed for your application to interface with ED-Auth.

1. Download the OpenLDAP Software Distribution. The OpenLDAP stable Release is always the best choice to install.

2. Configure OpenLDAP with the following line:

./configure --prefix=/path/to/openldap \
            --enable-slapd=no  \
            --enable-slurpd=no \
            --with-tls

If your OpenSSL libraries are not in the standard include and lib paths, you may need to run the following:

CPPFLAGS=-I/path/to/ssl/include LDFLAGS=-L/path/to/ssl/lib \
./configure --prefix=/path/to/openldap \
            --enable-slapd=no \ 
            --enable-slurpd=no \ 
            --with-tls

3. make depend && make && make install

Appendix: Resources

1) Note that this is a pam_ldap specific file, and it not used by OpenLDAP.
 
middleware/ed/edauth/usage.txt · Last modified: 2009/10/14 11:18 by dhawes
 
Except where otherwise noted, content on this wiki is licensed under the following license:CC Attribution-Noncommercial-Share Alike 3.0 Unported
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki